Permalink
Browse files

Merge branch '483-dispose-if-no-dependencies'

  • Loading branch information...
2 parents cdd585c + ce18b1c commit cb594d0a1d159cc06570be9e73f07e3fd63ee38f @SteveSanderson SteveSanderson committed Sep 7, 2012
@@ -211,6 +211,7 @@ describe('Dependent Observable', {
var computed1 = new ko.dependentObservable(function () { return underlyingObservable() + 1; });
var computed2 = new ko.dependentObservable(function () { return computed1.peek() + 1; });
value_of(computed2()).should_be(3);
+ value_of(computed2.isActive()).should_be(false);
underlyingObservable(11);
value_of(computed2()).should_be(3); // value wasn't changed
@@ -237,11 +238,55 @@ describe('Dependent Observable', {
);
value_of(timesEvaluated).should_be(1);
value_of(dependent.getDependenciesCount()).should_be(1);
+ value_of(dependent.isActive()).should_be(true);
timeToDispose = true;
underlyingObservable(101);
value_of(timesEvaluated).should_be(1);
value_of(dependent.getDependenciesCount()).should_be(0);
+ value_of(dependent.isActive()).should_be(false);
+ },
+
+ 'Should describe itself as active if the evaluator has dependencies on its first run': function() {
+ var someObservable = ko.observable('initial'),
+ dependentObservable = new ko.dependentObservable(function () { return someObservable(); });
+ value_of(dependentObservable.isActive()).should_be(true);
+ },
+
+ 'Should describe itself as inactive if the evaluator has no dependencies on its first run': function() {
+ var dependentObservable = new ko.dependentObservable(function () { return 123; });
+ value_of(dependentObservable.isActive()).should_be(false);
+ },
+
+ 'Should describe itself as inactive if subsequent runs of the evaluator result in there being no dependencies': function() {
+ var someObservable = ko.observable('initial'),
+ shouldHaveDependency = true,
+ dependentObservable = new ko.dependentObservable(function () { return shouldHaveDependency && someObservable(); });
+ value_of(dependentObservable.isActive()).should_be(true);
+
+ // Trigger a refresh
+ shouldHaveDependency = false;
+ someObservable('modified');
+ value_of(dependentObservable.isActive()).should_be(false);
+ },
+
+ 'Should register DOM node disposal callback only if active after the initial evaluation': function() {
+ // Set up an active one
+ var nodeForActive = document.createElement('DIV'),
+ observable = ko.observable('initial'),
+ activeDependentObservable = ko.dependentObservable({ read: function() { return observable(); }, disposeWhenNodeIsRemoved: nodeForActive });
+ var nodeForInactive = document.createElement('DIV')
+ inactiveDependentObservable = ko.dependentObservable({ read: function() { return 123; }, disposeWhenNodeIsRemoved: nodeForInactive });
+
+ value_of(activeDependentObservable.isActive()).should_be(true);
+ value_of(inactiveDependentObservable.isActive()).should_be(false);
+
+ // Infer existence of disposal callbacks from presence/absence of DOM data. This is really just an implementation detail,
+ // and so it's unusual to rely on it in a spec. However, the presence/absence of the callback isn't exposed in any other way,
+ // and if the implementation ever changes, this spec should automatically fail because we're checking for both the positive
+ // and negative cases.
+ value_of(ko.utils.domData.clear(nodeForActive)).should_be(true); // There was a callback
+ value_of(ko.utils.domData.clear(nodeForInactive)).should_be(false); // There was no callback
},
'Should advertise that instances *can* have values written to them if you supply a "write" callback': function() {
@@ -144,7 +144,7 @@
}
},
null,
- { 'disposeWhenNodeIsRemoved' : node }
+ { disposeWhenNodeIsRemoved : node }
);
return {
@@ -63,8 +63,8 @@
// of which nodes would be deleted if valueToMap was itself later removed
mappedNodes.splice(0, mappedNodes.length);
ko.utils.arrayPushAll(mappedNodes, newMappedNodes);
- }, null, { 'disposeWhenNodeIsRemoved': containerNode, 'disposeWhen': function() { return (mappedNodes.length == 0) || !ko.utils.domNodeIsAttachedToDocument(mappedNodes[0]) } });
- return { mappedNodes : mappedNodes, dependentObservable : dependentObservable };
+ }, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return (mappedNodes.length == 0) || !ko.utils.domNodeIsAttachedToDocument(mappedNodes[0]) } });
+ return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) };
}
var lastMappingResultDomDataKey = "setDomNodeChildrenFromArrayMapping_lastMappingResult";
@@ -121,7 +121,8 @@
mapData = lastMappingResult[lastMappingResultIndex];
// Stop tracking changes to the mapping for these nodes
- mapData.dependentObservable.dispose();
+ if (mapData.dependentObservable)
+ mapData.dependentObservable.dispose();
// Queue these nodes for later removal
nodesToDelete.push.apply(nodesToDelete, fixUpNodesToBeMovedOrRemoved(mapData.mappedNodes));
@@ -14,41 +14,20 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
if (!readFunction)
readFunction = options["read"];
}
- // By here, "options" is always non-null
if (typeof readFunction != "function")
throw new Error("Pass a function that returns the value of the ko.computed");
- var writeFunction = options["write"];
- if (!evaluatorFunctionTarget)
- evaluatorFunctionTarget = options["owner"];
+ function addSubscriptionToDependency(subscribable) {
+ _subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync));
+ }
- var _subscriptionsToDependencies = [];
function disposeAllSubscriptionsToDependencies() {
ko.utils.arrayForEach(_subscriptionsToDependencies, function (subscription) {
subscription.dispose();
});
_subscriptionsToDependencies = [];
}
- var dispose = disposeAllSubscriptionsToDependencies;
- // Build "disposeWhenNodeIsRemoved" and "disposeWhenNodeIsRemovedCallback" option values
- // (Note: "disposeWhenNodeIsRemoved" option both proactively disposes as soon as the node is removed using ko.removeNode(),
- // plus adds a "disposeWhen" callback that, on each evaluation, disposes if the node was removed by some other means.)
- var disposeWhenNodeIsRemoved = (typeof options["disposeWhenNodeIsRemoved"] == "object") ? options["disposeWhenNodeIsRemoved"] : null;
- var disposeWhen = options["disposeWhen"] || function() { return false; };
- if (disposeWhenNodeIsRemoved) {
- dispose = function() {
- ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, arguments.callee);
- disposeAllSubscriptionsToDependencies();
- };
- ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, dispose);
- var existingDisposeWhenFunction = disposeWhen;
- disposeWhen = function () {
- return !ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved) || existingDisposeWhenFunction();
- }
- }
-
- var evaluationTimeoutInstance = null;
function evaluatePossiblyAsync() {
var throttleEvaluationTimeout = dependentObservable['throttleEvaluation'];
if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
@@ -86,7 +65,7 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
if ((inOld = ko.utils.arrayIndexOf(disposalCandidates, subscribable)) >= 0)
disposalCandidates[inOld] = undefined; // Don't want to dispose this subscription, as it's still being used
else
- _subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync)); // Brand new subscription - add it
+ addSubscriptionToDependency(subscribable); // Brand new subscription - add it
});
var newValue = readFunction.call(evaluatorFunctionTarget);
@@ -107,7 +86,8 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
dependentObservable["notifySubscribers"](_latestValue);
_isBeingEvaluated = false;
-
+ if (!_subscriptionsToDependencies.length)
+ dispose();
}
function dependentObservable() {
@@ -128,26 +108,61 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
}
}
- dependentObservable.peek = function () {
+ function peek() {
if (!_hasBeenEvaluated)
evaluateImmediate();
return _latestValue;
}
+ function isActive() {
+ return !_hasBeenEvaluated || _subscriptionsToDependencies.length > 0;
+ }
+
+ // By here, "options" is always non-null
+ var writeFunction = options["write"],
+ disposeWhenNodeIsRemoved = options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null,
+ disposeWhen = options["disposeWhen"] || options.disposeWhen || function() { return false; },
+ dispose = disposeAllSubscriptionsToDependencies,
+ _subscriptionsToDependencies = [],
+ evaluationTimeoutInstance = null;
+
+ if (!evaluatorFunctionTarget)
+ evaluatorFunctionTarget = options["owner"];
+
+ dependentObservable.peek = peek;
dependentObservable.getDependenciesCount = function () { return _subscriptionsToDependencies.length; };
dependentObservable.hasWriteFunction = typeof options["write"] === "function";
dependentObservable.dispose = function () { dispose(); };
+ dependentObservable.isActive = isActive;
ko.subscribable.call(dependentObservable);
ko.utils.extend(dependentObservable, ko.dependentObservable['fn']);
- if (options['deferEvaluation'] !== true)
- evaluateImmediate();
-
ko.exportProperty(dependentObservable, 'peek', dependentObservable.peek);
ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose);
+ ko.exportProperty(dependentObservable, 'isActive', dependentObservable.isActive);
ko.exportProperty(dependentObservable, 'getDependenciesCount', dependentObservable.getDependenciesCount);
+ // Evaluate, unless deferEvaluation is true
+ if (options['deferEvaluation'] !== true)
+ evaluateImmediate();
+
+ // Build "disposeWhenNodeIsRemoved" and "disposeWhenNodeIsRemovedCallback" option values.
+ // But skip if isActive is false (there will never be any dependencies to dispose).
+ // (Note: "disposeWhenNodeIsRemoved" option both proactively disposes as soon as the node is removed using ko.removeNode(),
+ // plus adds a "disposeWhen" callback that, on each evaluation, disposes if the node was removed by some other means.)
+ if (disposeWhenNodeIsRemoved && isActive()) {
+ dispose = function() {
+ ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, arguments.callee);
+ disposeAllSubscriptionsToDependencies();
+ };
+ ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, dispose);
+ var existingDisposeWhenFunction = disposeWhen;
+ disposeWhen = function () {
+ return !ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved) || existingDisposeWhenFunction();
+ }
+ }
+
return dependentObservable;
};
@@ -107,7 +107,7 @@
}
},
null,
- { 'disposeWhen': whenToDispose, 'disposeWhenNodeIsRemoved': activelyDisposeWhenNodeIsRemoved }
+ { disposeWhen: whenToDispose, disposeWhenNodeIsRemoved: activelyDisposeWhenNodeIsRemoved }
);
} else {
// We don't yet have a DOM node to evaluate, so use a memo and render the template later when there is a DOM node
@@ -150,15 +150,15 @@
ko.utils.setDomNodeChildrenFromArrayMapping(targetNode, filteredArray, executeTemplateForArrayItem, options, activateBindingsCallback);
- }, null, { 'disposeWhenNodeIsRemoved': targetNode });
+ }, null, { disposeWhenNodeIsRemoved: targetNode });
};
- var templateSubscriptionDomDataKey = '__ko__templateSubscriptionDomDataKey__';
- function disposeOldSubscriptionAndStoreNewOne(element, newSubscription) {
- var oldSubscription = ko.utils.domData.get(element, templateSubscriptionDomDataKey);
- if (oldSubscription && (typeof(oldSubscription.dispose) == 'function'))
- oldSubscription.dispose();
- ko.utils.domData.set(element, templateSubscriptionDomDataKey, newSubscription);
+ var templateComputedDomDataKey = '__ko__templateComputedDomDataKey__';
+ function disposeOldComputedAndStoreNewOne(element, newComputed) {
+ var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey);
+ if (oldComputed && (typeof(oldComputed.dispose) == 'function'))
+ oldComputed.dispose();
+ ko.utils.domData.set(element, templateComputedDomDataKey, (newComputed && newComputed.isActive()) ? newComputed : undefined);
}
ko.bindingHandlers['template'] = {
@@ -190,25 +190,25 @@
shouldDisplay = shouldDisplay && !ko.utils.unwrapObservable(bindingValue['ifnot']);
}
- var templateSubscription = null;
+ var templateComputed = null;
if ((typeof bindingValue === 'object') && ('foreach' in bindingValue)) { // Note: can't use 'in' operator on strings
// Render once for each data point (treating data set as empty if shouldDisplay==false)
var dataArray = (shouldDisplay && bindingValue['foreach']) || [];
- templateSubscription = ko.renderTemplateForEach(templateName || element, dataArray, /* options: */ bindingValue, element, bindingContext);
+ templateComputed = ko.renderTemplateForEach(templateName || element, dataArray, /* options: */ bindingValue, element, bindingContext);
} else {
if (shouldDisplay) {
// Render once for this single data point (or use the viewModel if no data was provided)
var innerBindingContext = (typeof bindingValue == 'object') && ('data' in bindingValue)
? bindingContext['createChildContext'](ko.utils.unwrapObservable(bindingValue['data']), bindingValue['as']) // Given an explitit 'data' value, we create a child binding context for it
: bindingContext; // Given no explicit 'data' value, we retain the same binding context
- templateSubscription = ko.renderTemplate(templateName || element, innerBindingContext, /* options: */ bindingValue, element);
+ templateComputed = ko.renderTemplate(templateName || element, innerBindingContext, /* options: */ bindingValue, element);
} else
ko.virtualElements.emptyNode(element);
}
- // It only makes sense to have a single template subscription per element (otherwise which one should have its output displayed?)
- disposeOldSubscriptionAndStoreNewOne(element, templateSubscription);
+ // It only makes sense to have a single template computed per element (otherwise which one should have its output displayed?)
+ disposeOldComputedAndStoreNewOne(element, templateComputed);
}
};
View
@@ -33,7 +33,9 @@ ko.utils.domData = new (function () {
if (dataStoreKey) {
delete dataStore[dataStoreKey];
node[dataStoreKeyExpandoPropertyName] = null;
+ return true; // Exposing "did clean" flag purely so specs can infer whether things have been cleaned up as intended
}
+ return false;
}
}
})();

0 comments on commit cb594d0

Please sign in to comment.