More Optional chaining in Knockout.js

This commit is contained in:
the-djmaze 2022-09-13 22:00:31 +02:00
parent d18f93d87f
commit a566f6ed5a
10 changed files with 391 additions and 476 deletions

View file

@ -63,8 +63,6 @@ ko.computed = (evaluatorFunctionOrOptions, options) => {
state.pure = true;
state.isSleeping = true; // Starts off sleeping; will awake on the first subscription
ko.utils.extend(computedObservable, pureComputedOverrides);
} else if (options['deferEvaluation']) {
ko.utils.extend(computedObservable, deferEvaluationOverrides);
}
if (state.disposeWhenNodeIsRemoved) {
@ -82,10 +80,8 @@ ko.computed = (evaluatorFunctionOrOptions, options) => {
}
}
// Evaluate, unless sleeping or deferEvaluation is true
if (!state.isSleeping && !options['deferEvaluation']) {
computedObservable.evaluateImmediate();
}
// Evaluate, unless sleeping
state.isSleeping || computedObservable.evaluateImmediate();
// Attach a DOM node disposal callback so that the computed will be proactively disposed as soon as the node is
// removed using ko.removeNode. But skip if isActive is false (there will never be any dependencies to dispose).
@ -228,16 +224,13 @@ var computedFn = {
disposeWhen = state.disposeWhen,
changed = false;
if (state.isBeingEvaluated) {
// If the evaluation of a ko.computed causes side effects, it's possible that it will trigger its own re-evaluation.
// This is not desirable (it's hard for a developer to realise a chain of dependencies might cause this, and they almost
// certainly didn't intend infinite re-evaluations). So, for predictability, we simply prevent ko.computeds from causing
// their own re-evaluation. Further discussion at https://github.com/SteveSanderson/knockout/pull/387
return;
}
// If the evaluation of a ko.computed causes side effects, it's possible that it will trigger its own re-evaluation.
// This is not desirable (it's hard for a developer to realise a chain of dependencies might cause this, and they almost
// certainly didn't intend infinite re-evaluations). So, for predictability, we simply prevent ko.computeds from causing
// their own re-evaluation. Further discussion at https://github.com/SteveSanderson/knockout/pull/387
if (state.isBeingEvaluated
// Do not evaluate (and possibly capture new dependencies) if disposed
if (state.isDisposed) {
|| state.isDisposed) {
return;
}
@ -252,8 +245,8 @@ var computedFn = {
state.suppressDisposalUntilDisposeWhenReturnsFalse = false;
}
state.isBeingEvaluated = true;
try {
state.isBeingEvaluated = true;
changed = this.evaluateImmediate_CallReadWithDependencyDetection(notifyChange);
} finally {
state.isBeingEvaluated = false;
@ -324,7 +317,7 @@ var computedFn = {
return changed;
},
peek: function (evaluate) {
// By default, peek won't re-evaluate, except while the computed is sleeping or to get the initial value when "deferEvaluation" is set.
// By default, peek won't re-evaluate, except while the computed is sleeping.
// Pass in true to evaluate if needed.
var state = this[computedState];
if ((state.isDirty && (evaluate || !state.dependenciesCount)) || (state.isSleeping && this.haveDependenciesChanged())) {
@ -333,30 +326,31 @@ var computedFn = {
return state.latestValue;
},
limit: function (limitFunction) {
var self = this;
// Override the limit function with one that delays evaluation as well
ko.subscribable['fn'].limit.call(this, limitFunction);
this._evalIfChanged = function () {
if (!this[computedState].isSleeping) {
if (this[computedState].isStale) {
this.evaluateImmediate();
ko.subscribable['fn'].limit.call(self, limitFunction);
self._evalIfChanged = () => {
if (!self[computedState].isSleeping) {
if (self[computedState].isStale) {
self.evaluateImmediate();
} else {
this[computedState].isDirty = false;
self[computedState].isDirty = false;
}
}
return this[computedState].latestValue;
return self[computedState].latestValue;
};
this._evalDelayed = function (isChange) {
this._limitBeforeChange(this[computedState].latestValue);
self._evalDelayed = isChange => {
self._limitBeforeChange(self[computedState].latestValue);
// Mark as dirty
this[computedState].isDirty = true;
self[computedState].isDirty = true;
if (isChange) {
this[computedState].isStale = true;
self[computedState].isStale = true;
}
// Pass the observable to the "limit" code, which will evaluate it when
// it's time to do the notification.
this._limitChange(this, !isChange /* isDirty */);
self._limitChange(self, !isChange /* isDirty */);
};
},
dispose: function () {
@ -450,15 +444,6 @@ var pureComputedOverrides = {
}
};
var deferEvaluationOverrides = {
beforeSubscriptionAdd: function (event) {
// This will force a computed with deferEvaluation to evaluate when the first subscription is registered.
if (event == 'change' || event == 'beforeChange') {
this.peek();
}
}
};
// Note that for browsers that don't support proto assignment, the
// inheritance chain is created manually in the ko.computed constructor
Object.setPrototypeOf(computedFn, ko.subscribable['fn']);

View file

@ -22,25 +22,17 @@ ko.extenders['trackArrayChanges'] = (target, options) => {
// Watch "subscribe" calls, and for array change events, ensure change tracking is enabled
target.beforeSubscriptionAdd = event => {
if (underlyingBeforeSubscriptionAddFunction) {
underlyingBeforeSubscriptionAddFunction.call(target, event);
}
underlyingBeforeSubscriptionAddFunction?.call(target, event);
if (event === arrayChangeEventName) {
trackChanges();
}
};
// Watch "dispose" calls, and for array change events, ensure change tracking is disabled when all are disposed
target.afterSubscriptionRemove = event => {
if (underlyingAfterSubscriptionRemoveFunction) {
underlyingAfterSubscriptionRemoveFunction.call(target, event);
}
underlyingAfterSubscriptionRemoveFunction?.call(target, event);
if (event === arrayChangeEventName && !target.hasSubscriptionsForEvent(arrayChangeEventName)) {
if (changeSubscription) {
changeSubscription.dispose();
}
if (spectateSubscription) {
spectateSubscription.dispose();
}
changeSubscription?.dispose();
spectateSubscription?.dispose();
spectateSubscription = changeSubscription = null;
trackingChanges = false;
previousContents = undefined;
@ -48,23 +40,6 @@ ko.extenders['trackArrayChanges'] = (target, options) => {
};
function trackChanges() {
if (trackingChanges) {
// Whenever there's a new subscription and there are pending notifications, make sure all previous
// subscriptions are notified of the change so that all subscriptions are in sync.
notifyChanges();
return;
}
trackingChanges = true;
// Track how many times the array actually changed value
spectateSubscription = target.subscribe(() => ++pendingChanges, null, "spectate");
// Each time the array changes value, capture a clone so that on the next
// change it's possible to produce a diff
previousContents = [].concat(target.peek() || []);
cachedDiff = null;
changeSubscription = target.subscribe(notifyChanges);
function notifyChanges() {
if (pendingChanges) {
@ -86,6 +61,24 @@ ko.extenders['trackArrayChanges'] = (target, options) => {
}
}
}
if (trackingChanges) {
// Whenever there's a new subscription and there are pending notifications, make sure all previous
// subscriptions are notified of the change so that all subscriptions are in sync.
notifyChanges();
return;
}
trackingChanges = true;
// Track how many times the array actually changed value
spectateSubscription = target.subscribe(() => ++pendingChanges, null, "spectate");
// Each time the array changes value, capture a clone so that on the next
// change it's possible to produce a diff
previousContents = [].concat(target.peek() || []);
cachedDiff = null;
changeSubscription = target.subscribe(notifyChanges);
}
function getChanges(previousContents, currentContents) {
@ -126,20 +119,19 @@ ko.extenders['trackArrayChanges'] = (target, options) => {
case 'pop':
offset = arrayLength - 1;
case 'shift':
if (arrayLength) {
pushDiff('deleted', rawArray[offset], offset);
}
arrayLength && pushDiff('deleted', rawArray[offset], offset);
break;
case 'splice':
// Negative start index means 'from end of array'. After that we clamp to [0...arrayLength].
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
var startIndex = Math.min(Math.max(0, args[0] < 0 ? arrayLength + args[0] : args[0]), arrayLength),
endDeleteIndex = argsLength === 1 ? arrayLength : Math.min(startIndex + (args[1] || 0), arrayLength),
endAddIndex = startIndex + argsLength - 2,
var index = Math.min(Math.max(0, args[0] < 0 ? arrayLength + args[0] : args[0]), arrayLength),
endDeleteIndex = argsLength === 1 ? arrayLength : Math.min(index + (args[1] || 0), arrayLength),
endAddIndex = index + argsLength - 2,
endIndex = Math.max(endDeleteIndex, endAddIndex),
additions = [], deletions = [];
for (let index = startIndex, argsIndex = 2; index < endIndex; ++index, ++argsIndex) {
additions = [], deletions = [],
argsIndex = 2;
for (; index < endIndex; ++index, ++argsIndex) {
if (index < endDeleteIndex)
deletions.push(pushDiff('deleted', rawArray[index], index));
if (index < endAddIndex)

View file

@ -14,9 +14,8 @@ class koSubscription
dispose() {
var self = this;
if (!self._isDisposed) {
if (self._domNodeDisposalCallback) {
ko.utils.domNodeDisposal.removeDisposeCallback(self._node, self._domNodeDisposalCallback);
}
self._domNodeDisposalCallback
&& ko.utils.domNodeDisposal.removeDisposeCallback(self._node, self._domNodeDisposalCallback);
self._isDisposed = true;
self._disposeCallback();
@ -53,15 +52,12 @@ var ko_subscribable_fn = {
var subscription = new koSubscription(self, boundCallback, () => {
self._subscriptions.get(event).delete(subscription);
if (self.afterSubscriptionRemove)
self.afterSubscriptionRemove(event);
self.afterSubscriptionRemove?.(event);
});
if (self.beforeSubscriptionAdd)
self.beforeSubscriptionAdd(event);
self.beforeSubscriptionAdd?.(event);
if (!self._subscriptions.has(event))
self._subscriptions.set(event, new Set);
self._subscriptions.has(event) || self._subscriptions.set(event, new Set);
self._subscriptions.get(event).add(subscription);
return subscription;
@ -107,13 +103,13 @@ var ko_subscribable_fn = {
if (!self._origNotifySubscribers) {
self._origNotifySubscribers = self.notifySubscribers;
// Moved out of "limit" to avoid the extra closure
self.notifySubscribers = function(value, event) {
self.notifySubscribers = (value, event) => {
if (!event || event === defaultEvent) {
this._limitChange(value);
} else if (event === 'beforeChange') {
this._limitBeforeChange(value);
self._limitChange(value);
} else if (event === beforeChange) {
self._limitBeforeChange(value);
} else {
this._origNotifySubscribers(value, event);
self._origNotifySubscribers(value, event);
}
}
}