mirror of
https://github.com/Febbweiss/febbweiss.github.io.git
synced 2026-03-05 06:35:50 +00:00
500 lines
22 KiB
JavaScript
500 lines
22 KiB
JavaScript
var computedState = ko.utils.createSymbolOrString('_state');
|
|
|
|
ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, 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"],
|
|
evaluatorFunctionTarget: evaluatorFunctionTarget || options["owner"],
|
|
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") {
|
|
// Writing a value
|
|
writeFunction.apply(state.evaluatorFunctionTarget, arguments);
|
|
} else {
|
|
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.");
|
|
}
|
|
return this; // Permits chained assignments
|
|
} else {
|
|
// Reading the value
|
|
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'
|
|
if (!ko.utils.canSetPrototype) {
|
|
// 'subscribable' won't be on the prototype chain unless we put it there directly
|
|
ko.utils.extend(computedObservable, ko.subscribable['fn']);
|
|
}
|
|
ko.subscribable['fn'].init(computedObservable);
|
|
|
|
// Inherit from 'computed'
|
|
ko.utils.setPrototypeOfOrExtend(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 (ko.options['deferUpdates']) {
|
|
ko.extenders['deferred'](computedObservable, true);
|
|
}
|
|
|
|
if (DEBUG) {
|
|
// #1731 - Aid debugging by exposing the computed's options
|
|
computedObservable["_options"] = options;
|
|
}
|
|
|
|
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 = function () {
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
var computedFn = {
|
|
"equalityComparer": valuesArePrimitiveAndEqual,
|
|
getDependenciesCount: function () {
|
|
return this[computedState].dependenciesCount;
|
|
},
|
|
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 (dependencyTracking.hasOwnProperty(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) {
|
|
if (target._deferUpdates && !this[computedState].disposeWhenNodeIsRemoved) {
|
|
var dirtySub = target.subscribe(this.markDirty, this, 'dirty'),
|
|
changeSub = target.subscribe(this.respondToChange, this);
|
|
return {
|
|
_target: target,
|
|
dispose: function () {
|
|
dirtySub.dispose();
|
|
changeSub.dispose();
|
|
}
|
|
};
|
|
} else {
|
|
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 = ko.utils.setTimeout(function () {
|
|
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;
|
|
}
|
|
|
|
if (!state.dependenciesCount) {
|
|
computedObservable.dispose();
|
|
}
|
|
|
|
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 = this.evaluateImmediate_CallReadThenEndDependencyDetection(state, dependencyDetectionContext);
|
|
|
|
if (computedObservable.isDifferent(state.latestValue, newValue)) {
|
|
if (!state.isSleeping) {
|
|
computedObservable["notifySubscribers"](state.latestValue, "beforeChange");
|
|
}
|
|
|
|
state.latestValue = newValue;
|
|
if (DEBUG) computedObservable._latestValue = newValue;
|
|
|
|
if (state.isSleeping) {
|
|
computedObservable.updateVersion();
|
|
} else if (notifyChange) {
|
|
computedObservable["notifySubscribers"](state.latestValue);
|
|
}
|
|
|
|
changed = true;
|
|
}
|
|
|
|
if (isInitial) {
|
|
computedObservable["notifySubscribers"](state.latestValue, "awake");
|
|
}
|
|
|
|
return changed;
|
|
},
|
|
evaluateImmediate_CallReadThenEndDependencyDetection: function (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 {
|
|
var readFunction = state.readFunction;
|
|
return state.evaluatorFunctionTarget ? readFunction.call(state.evaluatorFunctionTarget) : 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;
|
|
}
|
|
},
|
|
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].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);
|
|
};
|
|
},
|
|
dispose: function () {
|
|
var state = this[computedState];
|
|
if (!state.isSleeping && state.dependencyTracking) {
|
|
ko.utils.objectForEach(state.dependencyTracking, function (id, dependency) {
|
|
if (dependency.dispose)
|
|
dependency.dispose();
|
|
});
|
|
}
|
|
if (state.disposeWhenNodeIsRemoved && state.domNodeDisposalCallback) {
|
|
ko.utils.domNodeDisposal.removeDisposeCallback(state.disposeWhenNodeIsRemoved, state.domNodeDisposalCallback);
|
|
}
|
|
state.dependencyTracking = null;
|
|
state.dependenciesCount = 0;
|
|
state.isDisposed = true;
|
|
state.isStale = false;
|
|
state.isDirty = false;
|
|
state.isSleeping = false;
|
|
state.disposeWhenNodeIsRemoved = null;
|
|
}
|
|
};
|
|
|
|
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 dependeciesOrder = [];
|
|
ko.utils.objectForEach(state.dependencyTracking, function (id, dependency) {
|
|
dependeciesOrder[dependency._order] = id;
|
|
});
|
|
// Next, subscribe to each one
|
|
ko.utils.arrayForEach(dependeciesOrder, function (id, order) {
|
|
var dependency = state.dependencyTracking[id],
|
|
subscription = computedObservable.subscribeToDependency(dependency._target);
|
|
subscription._order = order;
|
|
subscription._version = dependency._version;
|
|
state.dependencyTracking[id] = subscription;
|
|
});
|
|
}
|
|
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, function (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
|
|
if (ko.utils.canSetPrototype) {
|
|
ko.utils.setPrototypeOf(computedFn, ko.subscribable['fn']);
|
|
}
|
|
|
|
// Set the proto chain values for ko.hasPrototype
|
|
var protoProp = ko.observable.protoProperty; // == "__ko_proto__"
|
|
ko.computed[protoProp] = ko.observable;
|
|
computedFn[protoProp] = ko.computed;
|
|
|
|
ko.isComputed = function (instance) {
|
|
return ko.hasPrototype(instance, ko.computed);
|
|
};
|
|
|
|
ko.isPureComputed = function (instance) {
|
|
return ko.hasPrototype(instance, ko.computed)
|
|
&& instance[computedState] && instance[computedState].pure;
|
|
};
|
|
|
|
ko.exportSymbol('computed', ko.computed);
|
|
ko.exportSymbol('dependentObservable', ko.computed); // export ko.dependentObservable for backwards compatibility (1.x)
|
|
ko.exportSymbol('isComputed', ko.isComputed);
|
|
ko.exportSymbol('isPureComputed', ko.isPureComputed);
|
|
ko.exportSymbol('computed.fn', computedFn);
|
|
ko.exportProperty(computedFn, 'peek', computedFn.peek);
|
|
ko.exportProperty(computedFn, 'dispose', computedFn.dispose);
|
|
ko.exportProperty(computedFn, 'isActive', computedFn.isActive);
|
|
ko.exportProperty(computedFn, 'getDependenciesCount', computedFn.getDependenciesCount);
|
|
|
|
ko.pureComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget) {
|
|
if (typeof evaluatorFunctionOrOptions === 'function') {
|
|
return ko.computed(evaluatorFunctionOrOptions, evaluatorFunctionTarget, {'pure':true});
|
|
} else {
|
|
evaluatorFunctionOrOptions = ko.utils.extend({}, evaluatorFunctionOrOptions); // make a copy of the parameter object
|
|
evaluatorFunctionOrOptions['pure'] = true;
|
|
return ko.computed(evaluatorFunctionOrOptions, evaluatorFunctionTarget);
|
|
}
|
|
}
|
|
ko.exportSymbol('pureComputed', ko.pureComputed);
|