var computedState = Symbol('_state'); ko.computed = (evaluatorFunctionOrOptions, options) => { if (typeof evaluatorFunctionOrOptions === "object") { // Single-parameter syntax - everything is on this "options" param options = evaluatorFunctionOrOptions; } else { // Multi-parameter syntax - construct the options according to the params passed options = options || {}; if (evaluatorFunctionOrOptions) { options["read"] = evaluatorFunctionOrOptions; } } if (typeof options["read"] != "function") throw Error("Pass a function that returns the value of the ko.computed"); var writeFunction = options["write"]; var state = { latestValue: undefined, isStale: true, isDirty: true, isBeingEvaluated: false, suppressDisposalUntilDisposeWhenReturnsFalse: false, isDisposed: false, pure: false, isSleeping: false, readFunction: options["read"], disposeWhenNodeIsRemoved: options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null, disposeWhen: options["disposeWhen"] || options.disposeWhen, domNodeDisposalCallback: null, dependencyTracking: {}, dependenciesCount: 0, evaluationTimeoutInstance: null }; function computedObservable() { if (arguments.length > 0) { if (typeof writeFunction !== "function") { throw new Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters."); } // Writing a value writeFunction(...arguments); return this; // Permits chained assignments } else { // Reading the value state.isDisposed || ko.dependencyDetection.registerDependency(computedObservable); if (state.isDirty || (state.isSleeping && computedObservable.haveDependenciesChanged())) { computedObservable.evaluateImmediate(); } return state.latestValue; } } computedObservable[computedState] = state; computedObservable.hasWriteFunction = typeof writeFunction === "function"; // Inherit from 'subscribable' ko.subscribable['fn'].init(computedObservable); // Inherit from 'computed' Object.setPrototypeOf(computedObservable, computedFn); if (options['pure']) { 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) { // Since this computed is associated with a DOM node, and we don't want to dispose the computed // until the DOM node is *removed* from the document (as opposed to never having been in the document), // we'll prevent disposal until "disposeWhen" first returns false. state.suppressDisposalUntilDisposeWhenReturnsFalse = true; // disposeWhenNodeIsRemoved: true can be used to opt into the "only dispose after first false result" // behaviour even if there's no specific node to watch. In that case, clear the option so we don't try // to watch for a non-node's disposal. This technique is intended for KO's internal use only and shouldn't // be documented or used by application code, as it's likely to change in a future version of KO. if (!state.disposeWhenNodeIsRemoved.nodeType) { state.disposeWhenNodeIsRemoved = null; } } // Evaluate, unless sleeping or deferEvaluation is true if (!state.isSleeping && !options['deferEvaluation']) { 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). if (state.disposeWhenNodeIsRemoved && computedObservable.isActive()) { ko.utils.domNodeDisposal.addDisposeCallback(state.disposeWhenNodeIsRemoved, state.domNodeDisposalCallback = () => { computedObservable.dispose(); }); } return computedObservable; }; // Utility function that disposes a given dependencyTracking entry function computedDisposeDependencyCallback(id, entryToDispose) { if (entryToDispose !== null && entryToDispose.dispose) { entryToDispose.dispose(); } } // This function gets called each time a dependency is detected while evaluating a computed. // It's factored out as a shared function to avoid creating unnecessary function instances during evaluation. function computedBeginDependencyDetectionCallback(subscribable, id) { var computedObservable = this.computedObservable, state = computedObservable[computedState]; if (!state.isDisposed) { if (this.disposalCount && this.disposalCandidates[id]) { // Don't want to dispose this subscription, as it's still being used computedObservable.addDependencyTracking(id, subscribable, this.disposalCandidates[id]); this.disposalCandidates[id] = null; // No need to actually delete the property - disposalCandidates is a transient object anyway --this.disposalCount; } else if (!state.dependencyTracking[id]) { // Brand new subscription - add it computedObservable.addDependencyTracking(id, subscribable, state.isSleeping ? { _target: subscribable } : computedObservable.subscribeToDependency(subscribable)); } // If the observable we've accessed has a pending notification, ensure we get notified of the actual final value (bypass equality checks) if (subscribable._notificationIsPending) { subscribable._notifyNextChangeIfValueIsDifferent(); } } } function evaluateImmediate_CallReadThenEndDependencyDetection(state, dependencyDetectionContext) { // This function is really part of the evaluateImmediate_CallReadWithDependencyDetection logic. // You'd never call it from anywhere else. Factoring it out means that evaluateImmediate_CallReadWithDependencyDetection // can be independent of try/finally blocks, which contributes to saving about 40% off the CPU // overhead of computed evaluation (on V8 at least). try { return state.readFunction(); } finally { ko.dependencyDetection.end(); // For each subscription no longer being used, remove it from the active subscriptions list and dispose it if (dependencyDetectionContext.disposalCount && !state.isSleeping) { ko.utils.objectForEach(dependencyDetectionContext.disposalCandidates, computedDisposeDependencyCallback); } state.isStale = state.isDirty = false; } } var computedFn = { "equalityComparer": valuesArePrimitiveAndEqual, getDependenciesCount: function () { return this[computedState].dependenciesCount; }, getDependencies: function () { var dependencyTracking = this[computedState].dependencyTracking, dependentObservables = []; ko.utils.objectForEach(dependencyTracking, (id, dependency) => dependentObservables[dependency._order] = dependency._target ); return dependentObservables; }, hasAncestorDependency: function (obs) { if (!this[computedState].dependenciesCount) { return false; } var dependencies = this.getDependencies(); return dependencies.includes(obs) || !!dependencies.find(dep => dep.hasAncestorDependency && dep.hasAncestorDependency(obs)); }, addDependencyTracking: function (id, target, trackingObj) { if (this[computedState].pure && target === this) { throw Error("A 'pure' computed must not be called recursively"); } this[computedState].dependencyTracking[id] = trackingObj; trackingObj._order = this[computedState].dependenciesCount++; trackingObj._version = target.getVersion(); }, haveDependenciesChanged: function () { var id, dependency, dependencyTracking = this[computedState].dependencyTracking; for (id in dependencyTracking) { if (Object.prototype.hasOwnProperty.call(dependencyTracking, id)) { dependency = dependencyTracking[id]; if ((this._evalDelayed && dependency._target._notificationIsPending) || dependency._target.hasChanged(dependency._version)) { return true; } } } }, markDirty: function () { // Process "dirty" events if we can handle delayed notifications if (this._evalDelayed && !this[computedState].isBeingEvaluated) { this._evalDelayed(false /*isChange*/); } }, isActive: function () { var state = this[computedState]; return state.isDirty || state.dependenciesCount > 0; }, respondToChange: function () { // Ignore "change" events if we've already scheduled a delayed notification if (!this._notificationIsPending) { this.evaluatePossiblyAsync(); } else if (this[computedState].isDirty) { this[computedState].isStale = true; } }, subscribeToDependency: function (target) { return target.subscribe(this.evaluatePossiblyAsync, this); }, evaluatePossiblyAsync: function () { var computedObservable = this, throttleEvaluationTimeout = computedObservable['throttleEvaluation']; if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) { clearTimeout(this[computedState].evaluationTimeoutInstance); this[computedState].evaluationTimeoutInstance = setTimeout(() => computedObservable.evaluateImmediate(true /*notifyChange*/) , throttleEvaluationTimeout); } else if (computedObservable._evalDelayed) { computedObservable._evalDelayed(true /*isChange*/); } else { computedObservable.evaluateImmediate(true /*notifyChange*/); } }, evaluateImmediate: function (notifyChange) { var computedObservable = this, state = computedObservable[computedState], 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; } // Do not evaluate (and possibly capture new dependencies) if disposed if (state.isDisposed) { return; } if (state.disposeWhenNodeIsRemoved && !ko.utils.domNodeIsAttachedToDocument(state.disposeWhenNodeIsRemoved) || disposeWhen && disposeWhen()) { // See comment above about suppressDisposalUntilDisposeWhenReturnsFalse if (!state.suppressDisposalUntilDisposeWhenReturnsFalse) { computedObservable.dispose(); return; } } else { // It just did return false, so we can stop suppressing now state.suppressDisposalUntilDisposeWhenReturnsFalse = false; } state.isBeingEvaluated = true; try { changed = this.evaluateImmediate_CallReadWithDependencyDetection(notifyChange); } finally { state.isBeingEvaluated = false; } return changed; }, evaluateImmediate_CallReadWithDependencyDetection: function (notifyChange) { // This function is really just part of the evaluateImmediate logic. You would never call it from anywhere else. // Factoring it out into a separate function means it can be independent of the try/catch block in evaluateImmediate, // which contributes to saving about 40% off the CPU overhead of computed evaluation (on V8 at least). var computedObservable = this, state = computedObservable[computedState], changed = false; // Initially, we assume that none of the subscriptions are still being used (i.e., all are candidates for disposal). // Then, during evaluation, we cross off any that are in fact still being used. var isInitial = state.pure ? undefined : !state.dependenciesCount, // If we're evaluating when there are no previous dependencies, it must be the first time dependencyDetectionContext = { computedObservable: computedObservable, disposalCandidates: state.dependencyTracking, disposalCount: state.dependenciesCount }; ko.dependencyDetection.begin({ callbackTarget: dependencyDetectionContext, callback: computedBeginDependencyDetectionCallback, computed: computedObservable, isInitial: isInitial }); state.dependencyTracking = {}; state.dependenciesCount = 0; var newValue = evaluateImmediate_CallReadThenEndDependencyDetection(state, dependencyDetectionContext); if (!state.dependenciesCount) { computedObservable.dispose(); changed = true; // When evaluation causes a disposal, make sure all dependent computeds get notified so they'll see the new state } else { changed = computedObservable.isDifferent(state.latestValue, newValue); } if (changed) { if (!state.isSleeping) { computedObservable["notifySubscribers"](state.latestValue, "beforeChange"); } else { computedObservable.updateVersion(); } state.latestValue = newValue; computedObservable["notifySubscribers"](state.latestValue, "spectate"); if (!state.isSleeping && notifyChange) { computedObservable["notifySubscribers"](state.latestValue); } if (computedObservable._recordUpdate) { computedObservable._recordUpdate(); } } if (isInitial) { computedObservable["notifySubscribers"](state.latestValue, "awake"); } 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. // Pass in true to evaluate if needed. var state = this[computedState]; if ((state.isDirty && (evaluate || !state.dependenciesCount)) || (state.isSleeping && this.haveDependenciesChanged())) { this.evaluateImmediate(); } return state.latestValue; }, limit: function (limitFunction) { // 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(); } else { this[computedState].isDirty = false; } } return this[computedState].latestValue; }; this._evalDelayed = function (isChange) { this._limitBeforeChange(this[computedState].latestValue); // Mark as dirty this[computedState].isDirty = true; if (isChange) { this[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 */); }; }, dispose: function () { var state = this[computedState]; if (!state.isSleeping && state.dependencyTracking) { ko.utils.objectForEach(state.dependencyTracking, (id, dependency) => dependency.dispose && dependency.dispose() ); } if (state.disposeWhenNodeIsRemoved && state.domNodeDisposalCallback) { ko.utils.domNodeDisposal.removeDisposeCallback(state.disposeWhenNodeIsRemoved, state.domNodeDisposalCallback); } state.dependencyTracking = undefined; state.dependenciesCount = 0; state.isDisposed = true; state.isStale = false; state.isDirty = false; state.isSleeping = false; state.disposeWhenNodeIsRemoved = undefined; state.disposeWhen = undefined; state.readFunction = undefined; } }; var pureComputedOverrides = { beforeSubscriptionAdd: function (event) { // If asleep, wake up the computed by subscribing to any dependencies. var computedObservable = this, state = computedObservable[computedState]; if (!state.isDisposed && state.isSleeping && event == 'change') { state.isSleeping = false; if (state.isStale || computedObservable.haveDependenciesChanged()) { state.dependencyTracking = null; state.dependenciesCount = 0; if (computedObservable.evaluateImmediate()) { computedObservable.updateVersion(); } } else { // First put the dependencies in order var dependenciesOrder = []; ko.utils.objectForEach(state.dependencyTracking, (id, dependency) => dependenciesOrder[dependency._order] = id ); // Next, subscribe to each one dependenciesOrder.forEach((id, order) => { var dependency = state.dependencyTracking[id], subscription = computedObservable.subscribeToDependency(dependency._target); subscription._order = order; subscription._version = dependency._version; state.dependencyTracking[id] = subscription; }); // Waking dependencies may have triggered effects if (computedObservable.haveDependenciesChanged()) { if (computedObservable.evaluateImmediate()) { computedObservable.updateVersion(); } } } if (!state.isDisposed) { // test since evaluating could trigger disposal computedObservable["notifySubscribers"](state.latestValue, "awake"); } } }, afterSubscriptionRemove: function (event) { var state = this[computedState]; if (!state.isDisposed && event == 'change' && !this.hasSubscriptionsForEvent('change')) { ko.utils.objectForEach(state.dependencyTracking, (id, dependency) => { if (dependency.dispose) { state.dependencyTracking[id] = { _target: dependency._target, _order: dependency._order, _version: dependency._version }; dependency.dispose(); } }); state.isSleeping = true; this["notifySubscribers"](undefined, "asleep"); } }, getVersion: function () { // Because a pure computed is not automatically updated while it is sleeping, we can't // simply return the version number. Instead, we check if any of the dependencies have // changed and conditionally re-evaluate the computed observable. var state = this[computedState]; if (state.isSleeping && (state.isStale || this.haveDependenciesChanged())) { this.evaluateImmediate(); } return ko.subscribable['fn'].getVersion.call(this); } }; 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']); // Set the proto values for ko.computed var protoProp = ko.observable.protoProperty; // == "__ko_proto__" computedFn[protoProp] = ko.computed; ko.exportSymbol('computed', ko.computed); ko.exportSymbol('computed.fn', computedFn); ko.exportProperty(computedFn, 'dispose', computedFn.dispose); ko.pureComputed = (evaluatorFunctionOrOptions) => { if (typeof evaluatorFunctionOrOptions === 'function') { return ko.computed(evaluatorFunctionOrOptions, {'pure':true}); } evaluatorFunctionOrOptions = { ...evaluatorFunctionOrOptions }; // make a copy of the parameter object evaluatorFunctionOrOptions['pure'] = true; return ko.computed(evaluatorFunctionOrOptions); };