-
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
Conversation
bb4a26b
to
8b14e81
Compare
… "dirty" events to schedule the complete set of updates in the correct order and mark each computed observable as "dirty" so it can be evaluated if needed with the latest value.
8b14e81
to
baccd6e
Compare
Originally, in #1728, I thought that we could use the observable versioning system, which is used to efficiently to update sleeping pure computed observables, to accomplish a similar task for deferred updates. At the very least, I wanted to try it and compare it to using a So I went back to trying the |
expect(function() { | ||
jasmine.Clock.tick(1); | ||
}).toThrowContaining('Too much recursion'); | ||
}); |
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 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Right. I'd say that the previous behavior was undefined.
@SteveSanderson, I'm hoping to get your feedback here. This obviously involves some code changes within |
76c6364
to
048179c
Compare
expect(dependentComputed()).toEqual('C'); | ||
}); | ||
|
||
it('Should *not* delay update of dependent deferred computed observable', function () { |
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.
Thanks for this! Comments and questions added. |
Another point to consider before merging this is that "dirty" events will cause a computed to re-evaluate even if the underlying observable doesn't change. In my plugin, I dealt with this by having two levels of "dirty". A |
…rvables update on demand when dependent on deferred observables, maintaining similar behavior for rate-limited computeds between deferred and non-deferred usage. This commit also removes the ability to turn off deferred updates for an observable because it would result in an observable that appears to be rate-limited but actually updates synchronously.
…eWhenNodeIsRemoved option) won't respond to "dirty" events. This prevents conflicting bindings from causing recursive updates.
@SteveSanderson I've made some changes that address most of the concerns you brought up. Please check them over and give me your thoughts. |
@@ -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 comment
The 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 comment
The 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 comment
The 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.
…lse})" - it would have the opposite effect
Thanks again for implementing this, and sorry it's taken a few weeks to complete the review. This looks good to me - the notion of 'dirty' events propagating through any unbroken chain of deferreds to spread awareness that a change is coming, and hence that they should schedule a re-evaluation (and re-evaluate proactively if read) seems like a good way to model this. I'm happy for this to go in (so please merge it if you're ready, or let me know and I will), but I just want to ask a couple of clarifying questions in case you want to add further tweaks:
|
@SteveSanderson, I'm glad to get your feedback. Hopefully we can get this in soon.
One reason is that I realized that it creates a new class of observables that would need to be thoroughly tested (and the implementation probably adjusted accordingly). Another reason is that I decided that I didn't want to encourage turning off deferred updates for an individual observable when using the global
That's correct. Both use the internal
When using deferred updates globally, it still makes sense that people will sometimes want to use
We need to support setting |
Is deferring analogous to |
@brianmhunt That's a good question. If you use
|
@mbest Got it – that's very helpful (and probably worthwhile to reference in the docs!). |
Deferred updates, part 3: dependency tree scheduling
This is merged now. |
This is the third, and final, part of supporting deferred updates in Knockout (#1728).
With multiple deferred computed observables in a dependency tree, use "dirty" events to schedule the complete set of updates in the correct order and mark each computed observable as "dirty" so it can be evaluated if needed with the latest value.