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

Add skip element to hook object AND allow root DOM with children in patch #363

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

Conversation

bloodyKnuckles
Copy link

Allow the root DOM element to have children when used as original patch target: patch(document.getElementById('container'), vtree). Without this patch the original child would be ignored and therefore duplicated if included in the vtree. This problem can also be overcome with toVNode but not always preferable to use it.

Allow the root DOM element to have children when used as original patch target: `patch(document.getElementById('container'), vtree)`. Without this patch the original child would be ignored and therefore duplicated if included in the `vtree`. This problem can also be overcome with `toVNode` but not always preferable to use it.
@caridy
Copy link
Collaborator

caridy commented Feb 1, 2018

you can solve this today by doing:

patch(toVNode(document.getElementById('container')), vtree)

no changes needed.

@bloodyKnuckles
Copy link
Author

@caridy, do you see any problems adding a for loop in the emptyNodeAt function?

Some benefits from expanding the emptyNodeAt DOM conversion to include possible children in the root DOM element are: to simplify the interface, i.e. not having to figure out you need toVNode, and in situations where Snabbdom and it's patch function are buried within a hierarchy of modules and the patch implementation is limited or inaccessible, and if toVNode is not needed elsewhere it simplifies the program to not have to include it.

@caridy
Copy link
Collaborator

caridy commented Feb 1, 2018

@bloodyKnuckles yes, that's part of #359 (which is implemented by #362) in which such ambiguity is not longer a problem since you're forced to always use toVNode.

Add as "skip flagged element" test. Proposing adding "skip" flag to `hook` object to prevent `patch` function from overwriting the corresponding flagged DOM element.
Add a "skip element" flag to vnode `hook` object to prevent `patch` from overwriting a DOM element identified by a flagged vnode, e.g., `h('div#skipme', {hook: {skip: true}})`, will prevent `patch` from patching `<div id="skipme">preserve</div>`.
@bloodyKnuckles bloodyKnuckles changed the title Allow root DOM with children in patch Allow root DOM with children in patch AND add skip element to hook object Feb 1, 2018
@bloodyKnuckles bloodyKnuckles changed the title Allow root DOM with children in patch AND add skip element to hook object Add skip element to hook object AND allow root DOM with children in patch Feb 1, 2018
@bloodyKnuckles
Copy link
Author

bloodyKnuckles commented Feb 1, 2018

I have updated my Snabbdom fork by adding a "skip element" flag to the hook object. This allows preparing of a particular DOM element prior to Snabbdom patching when that element has complex HTML/JS and is manipulated by a separate process other than the Snabbdom patch that patches the skip element's parent DOM element. These are two separate issues but I don't know how to make these separate pull requests.

@caridy
Copy link
Collaborator

caridy commented Feb 2, 2018

@bloodyKnuckles you don't need any of these to just skip the element. You can create a simple vnode without data and/or children, and a unique key, and snabbdom will not touch it.

@bloodyKnuckles
Copy link
Author

bloodyKnuckles commented Feb 2, 2018

@caridy how does the key in the vnode correspond with the DOM element to be skipped? I'm using Snabbdom through CycleJS so access to the DOM element and Snabbdom are limited. For example, here's a code example: https://codesandbox.io/s/01qjkpwv5v How can the id="preserve" element be preserved with existing Snabbdom functionality as it is accessible through CycleJS? Keep in mind that the id="preserve" element is complex, changing, and manipulated by a separate process. So simply adding div('#preserve', 'preserve') to the VDOM stream does not accomplish the goal.

@caridy
Copy link
Collaborator

caridy commented Feb 2, 2018

@caridy how does the key in the vnode correspond with the DOM element to be skipped?

this is not really required! you can probably go without the key if there is no ambiguity with the siblings, that's all.

How can the id="preserve" element be preserved with existing Snabbdom functionality as it is accessible through CycleJS?

that's probably a question for @staltz. I'm just explaining how snabbdom works internally. If two vnodes are the same (via key, or selector), and there is no data, and no children specified in those vnodes, they modules will not attempt to mutate the element, which seems to be what you want. That means that you can have another process that makes changes to it (via a different patch routine, or just direct mutations on the DOM).

@bloodyKnuckles
Copy link
Author

@caridy I appreciate you looking at it. I think you've summed up my problem with "If two vnodes are the same...". The target DOM element is not the same as the new vnode. So my goal is to be able to skip patching an element regardless of vnode similarity. The simplest solution I can think of is a skip flag.

@caridy
Copy link
Collaborator

caridy commented Feb 2, 2018

@bloodyKnuckles maybe I don't understand that. Snabbdom is very simple, you have two children collection, the oldVnode.children and the newVnode.children, and you have to compare them, independently of what is really in the DOM (snabbdom assumes that oldVnode.children is an accurate representation of what is in the DOM at the moment of the patching).

now, if you want to maintain an element corresponding to a vnode from oldVnode.children, it must exists as a vnode in newVnode.children, and it must be distinguishable from the rest. attempting to just skip things from newVnode.children as if it wasn't part of the children collection in the first place can have undesirable results because the diffing algo may decide to add things before or after, so the final result is a non-deterministic order of childNodes. That's why we are suggesting to use the vnode identify in old and new vnode to produce the exact output that you want.

Add more tests to core tests to determine the effect the skip flag has on node order.
@bloodyKnuckles
Copy link
Author

...the diffing algo may decide to add things before or after, so the final result is a non-deterministic order of childNodes.

@caridy I agree, this is important. I added 5 more tests that reorder, add, and remove nodes—all pass. I'm trying to think of more potential pitfalls. Can you think of any more situations that need testing?

@caridy
Copy link
Collaborator

caridy commented Feb 2, 2018

It is hard to tell because I haven't understand what are you trying to do. Those changes are definitely not something that we can do because it will impact the perf by adding those extra checks and string computations during the critical path, so there must be another way to do what you want/need, but someone has to understand those requirements so we can help you.

@bloodyKnuckles
Copy link
Author

bloodyKnuckles commented Feb 3, 2018

yes, performance is another important consideration. Regarding the "skip element" flag, if there is no skip flag in the code the only additional code to run is:

'undefined' !== typeof vnode.data && hasSkip(vnode.data)

hasSkip is:

function hasSkip(s) { return isDef(s = s.hook) && isDef(s = s.skip) && s === true; }

The performance hit boils down to:

'undefined' !== typeof vnode.data 
  && isDef(s = s.hook) 
  && isDef(s = s.skip)

How is Snabbdom performance tested? That'd be nice to know the performance hit these checks bring.

Regarding purpose, simply put, I want to create a "black hole" in the VDOM that corresponds to a DOM element. The reason for this is to allow an element to be manipulated by a separate process other than the one that manipulates the DOM surrounding the "black hole". If I was only using Snabbdom toVNode does the trick. However I'm using CycleJS and don't have the same kind of direct access to the DOM when building the VDOM.

@staltz
Copy link
Member

staltz commented Feb 5, 2018

@bloodyKnuckles I think what you want to achieve can be done with thunks (see the thunk docs in snabbdom). They basically exist to enable a hook into "patch".

@bloodyKnuckles
Copy link
Author

bloodyKnuckles commented Feb 5, 2018 via email

@bloodyKnuckles
Copy link
Author

@staltz The problem I have with thunk is it overwrites the pre-existing DOM element.

For example, plain Snabbdom: https://codepen.io/anon/pen/WMxzQe?editors=1010
And CycleJS: https://codesandbox.io/s/k229v302vv

I need a way to tell Snabbdom to skip the element entirely, even on initial vtree render. I've also looked at the various hooks, init, prepatch, create, etc. but have not found a way with Snabbdom as-is to create this "black hole" in the VDOM, (other than toVNode but as mentioned I don't see how to access the DOM element in a CycleJS cycle in order toVNode it.

@staltz
Copy link
Member

staltz commented Feb 7, 2018

@bloodyKnuckles You're getting close.

In snabbdom, a patch is avoided when the next vnode is the same object as prev vnode. The initial patch, as you said, occurs by default because the prev vnode in this case is actually a so-called emptyVNode, and emptyVNode === myFirstVNode usually evaluates to false.

To fix that, you need to use toVNode so that the first comparison is not with emptyVNode. If the diff always occurs with the same vnode object, no patch will ever happen. So you need to create that constant vnode once, with toVNode, then use it in the thunk every time:

var patch = snabbdom.init([]), h = snabbdom.h, p1;
var tovnode = tovnode.default
var thunk = snabbdom.thunk
var contentTree = tovnode(document.getElementById('container'))
var middleEl = document.getElementById('middle')
var vmiddle = tovnode(middleEl)

console.log(vmiddle);

var vtree = h('div#container', [
  h('div#top', 'a'),
  thunk('div#middle', (a) => vmiddle, ['donotwritethis']),
  h('div#bottom', 'c'),
])
p1 = patch(contentTree, vtree)

I tested this in the codesanbox and it worked. :)

@bloodyKnuckles
Copy link
Author

@staltz I have experimented with toVNode but have not figured out how to access the skippable DOM element within a CycleJS app: https://codesandbox.io/s/k229v302vv

@bloodyKnuckles
Copy link
Author

bloodyKnuckles commented Feb 8, 2018

Curious about the performance hit, I ran this test with and without the proposed hasSkip hook:

var t0 = performance.now(), ii = 0
var vtree = sh('div#app', [
  sh('div#one', 'a'),
  sh('div#two', 'b'),
  sh('div#three', 'c')
])
p1 = patch(document.getElementById('app'), vtree)

while ( 1000000 > ii ) {
  var vtree = sh('div#app', [
    sh('div#one', 'a ' + ii),
    sh('div#two', 'b ' + ii),
    sh('div#three', 'c ' + ii)
  ])
  p1 = patch(p1, vtree)
  ii++
}
var t1 = performance.now()
console.log(t1-t0)

I ran the test 10 times, switched bundles, ran 10, switched bundles, ran 10, switched bundles, ran 10. And here's the results I got:

hasSkip:  3214.5999999993364
hasSkip:  3143.6000000030617
hasSkip:  2923.6999999993714
hasSkip:  3179.7000000005937
hasSkip:  3099.39999999915
hasSkip:  3126.299999996263
hasSkip:  2972.3999999987427
hasSkip:  3102.999999995518
hasSkip:  2979.800000000978
hasSkip:  3230.8999999950174
sd:  2855.0000000032014
sd:  2988.2999999972526
sd:  2882.7999999994063
sd:  2970.000000001164
sd:  2970.900000000256
sd:  3062.2999999977765
sd:  2949.1999999954714
sd:  2965.7999999981257
sd:  3011.500000000524
sd:  3000
hasSkip:  2906.4999999973224
hasSkip:  2932.0000000006985
hasSkip:  2813.6000000013155
hasSkip:  3036.2999999997555
hasSkip:  2932.700000004843
hasSkip:  3059.500000003027
hasSkip:  3131.3000000009197
hasSkip:  3051.500000001397
hasSkip:  2967.499999998836
hasSkip:  3118.399999999383
sd:  2966.3000000000466
sd:  2742.7000000025146
sd:  2939.699999995355
sd:  2776.5999999974156
sd:  3036.2999999997555
sd:  3042.699999998149
sd:  3004.800000002433
sd:  2969.2999999970198
sd:  3104.0999999968335
sd:  3026.2000000002445

hasSkip AVG:	3046.1349999998 (3097.3399999988/2994.9300000007)
sd AVG:		2963.2249999991 (2965.5799999993/2960.869999999)

And here's the test bundles DIFF:

144a145
> function hasSkip(s) { return isDef(s = s.hook) && isDef(s = s.skip) && s === true; }
145a147,149
> function sameVnodeId(vnode1Sel, vnode2Sel) {
>     return vnode1Sel.indexOf('#') > -1 && vnode1Sel.split('.')[0] === vnode2Sel.split('.')[0];
> }
147c151,154
<     return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
---
>     return ((vnode1.key === vnode2.key && vnode1.sel === vnode2.sel)
>         || ('undefined' !== typeof vnode2.data && hasSkip(vnode2.data)
>             && 'undefined' !== typeof vnode1.sel && 'undefined' !== typeof vnode2.sel
>             && sameVnodeId(vnode1.sel, vnode2.sel)));
183,184c190,197
<         var c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
<         return vnode_1.default(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
---
>         var c = elm.className && 'string' === typeof elm.className ? '.' + elm.className.split(' ').join('.') : '';
>         var elmvnode = vnode_1.default(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
>         for (var child = elm.firstChild, ii = 0; child !== null; child = child.nextSibling, ii++) {
>             if (1 === child.nodeType && 'undefined' !== typeof elmvnode.children) {
>                 elmvnode.children[ii] = emptyNodeAt(child);
>             }
>         }
>         return elmvnode;
384c397
<         if (oldVnode === vnode)
---
>         if (oldVnode === vnode || ('undefined' !== typeof vnode.data && hasSkip(vnode.data)))
507,510c520,523
< //var init = require('../../modules/snabbdom/snabbdom.js').init
< //var sh = require('../../modules/snabbdom/h.js').h
< var init = require('../snabbdom/snabbdom.js').init
< var sh = require('../snabbdom/h.js').h
---
> var init = require('../../modules/snabbdom/snabbdom.js').init
> var sh = require('../../modules/snabbdom/h.js').h
> //var init = require('../snabbdom/snabbdom.js').init
> //var sh = require('../snabbdom/h.js').h
531c544
< console.log('sd: ', t1-t0)
---
> console.log('hasSkip: ', t1-t0)
533c546
< },{"../snabbdom/h.js":1,"../snabbdom/snabbdom.js":4}]},{},[7]);
---
> },{"../../modules/snabbdom/h.js":1,"../../modules/snabbdom/snabbdom.js":4}]},{},[7]);

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.

None yet

3 participants