Skip to content
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

Reuse updated nodes and contexts. #46

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

AllSeeingEye
Copy link

@AllSeeingEye AllSeeingEye commented Jan 4, 2017

This is the initial attempt to avoid costly add/remove DOM operations by reusing DOM nodes and the respective context(s), as discussed in #37.

This patch still relies on the arrayChange; script now detects add/delete sequence on the same index and replaces it with a single 'update' operation. This, however, causes issues with some tests that rely on DOM node identity - please see my changes to spec\knockout-fast-foreach-spec.js (I wasn't able to fix all the test issues, unfortunately).

In the React/Angular/Knockout/raw test:

https://brianmhunt.github.io/chrisharrington-performance.html

this new version is about 9 times faster (for the update operation, not for the initial array deployment; 5000 elements) than the current master branch code.

Future work: we should probably reuse all the possible nodes, only adding/removing the tail ones as necessary (and caching nodes, maybe?).

@cervengoc
Copy link
Collaborator

Hello @AllSeeingEye, thank you for your work. At first glance the idea seems to be good, however I have some worries, basically two.

First, I don't know if it's supported to change an existing bindingContext like this. I mean just simply setting the $data and the $rawData members. It seems to be dangerous and I have a feeling that it can have unwanted side-effects. But honestly I'm not so competent in deep knockout behavior, so @brianmhunt you should check this too please.

Second, reusing the active nodes of a previous bindingContext is not so simple in more complex scenarios. I don't have time currently to think it over, but please consider this following use case.

<ul data-bind="fastForEach: Items">
  <li>
    <span data-bind="text: Caption"></span>
    <div data-bind="visible: Details, with: Details">
      <span data-bind="text: DetailCaption"></span>
      ... some other valuable nodes
    </div>
  </li> 
</ul>

The key here is the with binding. If the Detail child object of an item is null, the with binding will remove the nodes. But I don't think it will restore them on ko.cleanNode. So next time you want to apply the item bindings, you'll loose the content nodes of the div with the with binding. This might not be true at all, it's just an assumption, and a scenario for you to think about.

The problem is not only and not exactly with the with binding, but with the fact, that several bindings (especially custom ones, but also some built-in) will manipulate the nodes dynamically based on their data, and they won't clean up after themselves when calling ko.cleanNode, but only when the node is removed (see the domNodeDisposalCallback feature).

So basically while I like this idea, I think it's suitable only for simple cases, where one uses a constant node set as the fastForEach template, which is always guaranteed to be just simply rebindable without loosing its integrity. Still, it would probably make sense to introduce this as an opt-in behavior for thses scenarios, because the performance boost can indeed be great.

@brianmhunt
Copy link
Owner

@cervengoc Yep, behaviour when modifying bindingContext would be not well tested for many bindings. I would classify this as "experimental".

The nested re-use is definitely an issue, so I would definitely consider this an optional parameter (defaulting to false/off).

The afterAdd test would also need to be fixed.

That said, the performance improvement is pretty spectacular, so if it can be done - even for optional simple cases that would make some people's lives great.

If we go down the optional flag path, what would be a good name for the flag? reuseBindingContext?

@brianmhunt
Copy link
Owner

An alternative, (that may or may not be) worth considering, is forking fast-foreach and creating another binding for simple cases, that extends the FastForEach class. Just in case we're getting too complex here with experimental options / etc. The big advantage is that you wouldn't have to duplicate every test with the flag enabled (which is something perhaps needed to ensure that it works as expected in all the same cases as fast-foreach).

@cervengoc
Copy link
Collaborator

I like the fork idea, I was thinking about that too, following the pattern of the already discussed with and using (aka. "light" with) binding, which deals with somewhat similar concepts.

If with / using will make it to release, then we should probably name it also as foreach and foreachUsing.

@brianmhunt
Copy link
Owner

The with / using bindings are in tko.

I think foreachUsing works, unless we thing of something cleverer. 😄

@@ -421,7 +421,7 @@ describe("observable array changes", function () {
arr[i].num = i;
}
assert.equal(div.children().length, itemNumber)
assert.equal(div.children().filter(function() { return this.testprop == 10; }).length, itemNumber)
//assert.equal(div.children().filter(function() { return this.testprop == 10; }).length, itemNumber)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the issue here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is strange. I think that test script doesn't catch up with the rendering speed (?), as I get different assertion results every time - it is in the range of 97-100. Gimme a bit of time to look into this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test passes if I wrap the 2nd part of the script (everything after console.timeEnd("with move");) with a setTimeout(function() { ... }, 1). What do you think of this?

@@ -571,7 +571,7 @@ describe("observable array changes", function () {
})

describe('afterAdd', function () {
it("emits on changes to an observable array", function () {
it.skip("emits on changes to an observable array", function () {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll want to re-enable this before merging (unless there's a compelling reason to disable it)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The very last assertion in this test:

assert.equal(nodes, 4, 'n4')

needs to be rewritten as:

assert.equal(nodes, 3, 'n4')

This fixes the test.

@AllSeeingEye
Copy link
Author

AllSeeingEye commented Jan 6, 2017

@cervengoc: thank you for your comment; I felt that handling the binding context(s) the way I did was dangerous - I just wanted to get an input on this. I've moved to a safer code in my latest commits.

I need to think more on your comment on binding update/cleanup, thanks for the heads up.

@Scarbous
Copy link

Scarbous commented May 6, 2022

Thanks, that solved my flickering problem.
Why is it still not merged?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants