-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deferred updates, part 3: dependency tree scheduling #1795
Changes from 5 commits
baccd6e
6d5d786
048179c
72d5c43
a0134f5
4ca0819
f5a4d17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -125,4 +125,14 @@ describe("Deferred bindings", function() { | |
expect(testNode.childNodes[0]).toContainHtml('<span data-bind="text: childprop">moving child</span><span data-bind="text: childprop">first child</span><span data-bind="text: childprop">second child</span>'); | ||
expect(testNode.childNodes[0].childNodes[targetIndex]).not.toBe(itemNode); // node was create anew so it's not the same | ||
}); | ||
|
||
it('Should not throw an exception for value binding on multiple select boxes', function() { | ||
testNode.innerHTML = "<select data-bind=\"options: ['abc','def','ghi'], value: x\"></select><select data-bind=\"options: ['xyz','uvw'], value: x\"></select>"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that the non-deferred scenario doesn't throw an exception, I decided to give this some more thought. I realized that if the computed observables used for updating the binding don't respond to "dirty" events, that would stop the recursion as long as the underlying observable suppresses notifications for non-changing updates. So this will still cause a "recursion" exception if the values are objects instead of strings. @SteveSanderson, do you think this change is helpful or not? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this brings the deferred behavior more into line with non-deferred, then I guess it's a good thing, but it's hard to think of a case where someone would be doing this and expect something useful to happen :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we may need to take a closer look at how bindings interact with the deferred updates feature, as you've described in #1758. So this code may need to change some more anyway. |
||
var observable = ko.observable(); | ||
expect(function() { | ||
ko.applyBindings({ x: observable }, testNode); | ||
jasmine.Clock.tick(1); | ||
}).not.toThrow(); | ||
expect(observable()).not.toBeUndefined(); // The spec doesn't specify which of the two possible values is actually set | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test presents a concrete example of how the recursive task check could come into play (and is the reason I added the check in the deferred updates plugin). It's interesting, though, that this example doesn't result in an infinite loop before the code changes here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you regarding this as not a breaking change because the previous behavior was not well defined? I'm not sure what the previous behavior would have been, as it does seem impossible for 'x' to satisfy the requirements of both 'select' boxes simultaneously. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right. I'd say that the previous behavior was undefined. |
||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,6 +60,5 @@ ko.computedContext = ko.dependencyDetection = (function () { | |
ko.exportSymbol('computedContext', ko.computedContext); | ||
ko.exportSymbol('computedContext.getDependenciesCount', ko.computedContext.getDependenciesCount); | ||
ko.exportSymbol('computedContext.isInitial', ko.computedContext.isInitial); | ||
ko.exportSymbol('computedContext.isSleeping', ko.computedContext.isSleeping); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed that we were exporting a non-existent property, so I took it out. |
||
ko.exportSymbol('ignoreDependencies', ko.ignoreDependencies = ko.dependencyDetection.ignore); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,15 +57,45 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva | |
isSleeping = false; | ||
} | ||
|
||
function subscribeToDependency(target) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function will be quite a lot more expensive than the usual 'subscribe' if we go into the 'deferred' code path, both in memory and CPU use, as now it constructs several extra function instances with their own closures. This once for each dependency, every time any 'deferred' computed is re-evaluated. It might be OK, but is there any possibility of dropping these dynamically created closures in favour of static function instances and variables that get mutated? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had originally written this with static functions but then realized I needed to access |
||
if (target._deferUpdates && !disposeWhenNodeIsRemoved) { | ||
var dirtySub = target.subscribe(markDirty, null, 'dirty'), | ||
changeSub = target.subscribe(respondToChange); | ||
return { | ||
_target: target, | ||
dispose: function () { | ||
dirtySub.dispose(); | ||
changeSub.dispose(); | ||
} | ||
}; | ||
} else { | ||
return target.subscribe(evaluatePossiblyAsync); | ||
} | ||
} | ||
|
||
function markDirty() { | ||
// Process "dirty" events if we can handle delayed notifications | ||
if (dependentObservable._evalDelayed && !_isBeingEvaluated) { | ||
dependentObservable._evalDelayed(); | ||
} | ||
} | ||
|
||
function respondToChange() { | ||
// Ignore "change" events if we've already scheduled a delayed notification | ||
if (!dependentObservable._notificationIsPending) { | ||
evaluatePossiblyAsync(); | ||
} | ||
} | ||
|
||
function evaluatePossiblyAsync() { | ||
var throttleEvaluationTimeout = dependentObservable['throttleEvaluation']; | ||
if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) { | ||
clearTimeout(evaluationTimeoutInstance); | ||
evaluationTimeoutInstance = ko.utils.setTimeout(function () { | ||
evaluateImmediate(true /*notifyChange*/); | ||
}, throttleEvaluationTimeout); | ||
} else if (dependentObservable._evalRateLimited) { | ||
dependentObservable._evalRateLimited(); | ||
} else if (dependentObservable._evalDelayed) { | ||
dependentObservable._evalDelayed(); | ||
} else { | ||
evaluateImmediate(true /*notifyChange*/); | ||
} | ||
|
@@ -115,7 +145,7 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva | |
--disposalCount; | ||
} else if (!dependencyTracking[id]) { | ||
// Brand new subscription - add it | ||
addDependencyTracking(id, subscribable, isSleeping ? { _target: subscribable } : subscribable.subscribe(evaluatePossiblyAsync)); | ||
addDependencyTracking(id, subscribable, isSleeping ? { _target: subscribable } : subscribeToDependency(subscribable)); | ||
} | ||
} | ||
}, | ||
|
@@ -230,14 +260,14 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva | |
var originalLimit = dependentObservable.limit; | ||
dependentObservable.limit = function(limitFunction) { | ||
originalLimit.call(dependentObservable, limitFunction); | ||
dependentObservable._evalRateLimited = function() { | ||
dependentObservable._rateLimitedBeforeChange(_latestValue); | ||
dependentObservable._evalDelayed = function() { | ||
dependentObservable._limitBeforeChange(_latestValue); | ||
|
||
_needsEvaluation = true; // Mark as dirty | ||
|
||
// Pass the observable to the rate-limit code, which will access it when | ||
// Pass the observable to the "limit" code, which will access it when | ||
// it's time to do the notification. | ||
dependentObservable._rateLimitedChange(dependentObservable); | ||
dependentObservable._limitChange(dependentObservable); | ||
} | ||
}; | ||
|
||
|
@@ -262,7 +292,7 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva | |
// Next, subscribe to each one | ||
ko.utils.arrayForEach(dependeciesOrder, function(id, order) { | ||
var dependency = dependencyTracking[id], | ||
subscription = dependency._target.subscribe(evaluatePossiblyAsync); | ||
subscription = subscribeToDependency(dependency._target); | ||
subscription._order = order; | ||
subscription._version = dependency._version; | ||
dependencyTracking[id] = subscription; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This subtlety - how notifications and re-evaluations differ between regular, ratelimited, and deferred computeds is very subtle indeed and is going to require careful attention in docs. Probably some big table of comparisons, with notes that provide justifications for all the differences.
I think I can guess why these differences exist, but it was still surprising to me at first.