Closed dy closed 4 years ago
not ideal, is it resulting in wrong behavior in the benchmark?
It breaks some algos that depend on checking nextSibling
, like swapping items etc.
In particular, than bug doesn't allow this 235b implementation to be qualified:
export function merge (parent, a, b, before) {
const bs = new Set(b), as = new Set(a)
let i, ai, bi, off
// walk by b from tail
// a: 1 2 3 4 5, b: 1 2 3 → off: +2
for (i = b.length, off = a.length - b.length; i--;) {
ai = a[i + off], bi = b[i]
if (ai === bi) {}
// replace
else if (ai && !as.has(bi) && !bs.has(ai)) (parent.replaceChild(bi, ai))
// remove
else if (ai && !bs.has(ai)) (parent.removeChild(ai), off--, i++)
else if (bi.nextSibling != before || !bi.nextSibling) {
// swap
if (as.has(bi)) (parent.insertBefore(ai, bi), off--)
// insert
parent.insertBefore(bi, before), off++
}
before = bi
}
return b
}
In real dom it works brilliant.
I couldn't manage to figure out why there's _childNodes
with extra element and fix that.
Interestingly, list-difference is broken in real DOM due to that reason.
Ok, just in case - reproduced benchmark in DOM fully: https://github.com/spectjs/spect/blob/h-reducers/test/diff.js Libs seems to be broken on real DOM, unfortunately.
great find, which ones are broken?
I've tested list-difference
, snabbdom
, udomdiff
- all algos seem to fail on real DOM (I've just reproduced benchmark tests). It's due to that childNodes.lastElement
thing I suppose.
Just in case - there are multiple various DOM implementations for nodejs:
hmm that would be strange, that all would be failing.
yea good idea, I haven't thought of using a more robust DOM library. I'll look into some. I guess we would want to keep the operation count but that could be patched on maybe.
ah I see you removed the before
node completely. yes I think some algorithms really rely on that to be there.
also wanted to mention sometimes spect fails the assertion of the shuffled test. not a lot but once every 3 or 4 times.
Thanks for letting know. I caught similar error rewriting the thing today, basically nextSibling
array was error-prone. The 232b version doesn't have that issue:
I couldn't manage to figure out why there's
_childNodes
with extra element and fix that.
As the benchmark was born after a file which only purpose was to code-cover synthetically udomdiff, benchmark that simply reproduces what js-framework-benchmark showcases, the last element is needed to pin the operation in a specific point.
Here how it works:
<body>
<!--pin-->
</body>
That pin comment node, makes it possible to confine all operations before it.
var newNodes = udomdiff(
document.body,
[],
[document.createElement('a'), document.createElement('b')],
pin
);
After this operation, the layout would look like:
<body>
<a></a><b></b>
<!--pin-->
</body>
The pin basically confines operations because otherwise, if a node that should be inserted but it's not live yet, uses nextSibling
, and the value is null
, the insertBefore
operation would append that node at the end of the parent container, and the end can be after any other node.
udomdiff is a standalone library that covers all real-world uses cases I could try, so if there's a scenario where it fails in the real DOM, but again, it's heavily tested against that, an issue filed in udomdiff repository would be much better than discussing in here the Dommy facade, as that's, once again, there only to synthetically quickly test and benchmark the library, it was never meant to be used across multiple libraries.
You can try basichtml or jsdom instead, if you want a more reliable DOM env, or you can rewrite Dommy as such:
class Dommy extends Array {
constructor() {
super().operations = [];
}
get childNodes() {
return this.slice(0);
}
get textContent() {
return this.map(node => node.value).join('');
}
insertBefore(newNode, liveNode) {
// add operation
if (newNode === liveNode)
return;
this._remove(newNode);
if (liveNode == null)
this.push(newNode);
else {
const i = this.indexOf(liveNode);
if (i < 0)
throw new Error('invalid insertBefore');
this.splice(i, 0, newNode);
}
newNode.dommy = this;
}
replaceChild(newNode, oldNode) {
// add operation
this._remove(newNode);
const i = this.indexOf(oldNode);
if (i < 0)
throw new Error('invalid replaceChild');
this[i] = newNode;
newNode.dommy = this;
oldNode.dommy = [];
}
removeChild(node) {
// add operation
if (!this.includes(node))
throw new Error('invalid removeChild');
this._remove(node);
node.dommy = [];
}
_remove(node) {
const i = this.indexOf(node);
if (i < 0)
this.splice(i, 1);
}
// previously ...
// count() { return this.length; }
// reset() { this.splice(0); }
}
class Nody {
constructor(value) {
this.dommy = [];
this.value = value;
}
get nextSibling() {
const i = this.dommy.indexOf(this) + 1;
return 0 < i && i < this.dommy.length ? this.dommy[i] : null;
}
}
At that point you can either do an initial push of a comment node and pass it along, verifying that it never gets removed and it's always the last element, or you can just drop the before
part, as this particular benchmark doesn't need it.
I would prefer to keep the pin concept, as diffing without that ability, in the real-world is kinda useless, 'cause handling all nodes per each container, is not really how libraries applied to the DOM work.
The pin basically confines operations because otherwise, if a node that should be inserted but it's not live yet, uses nextSibling, and the value is null, the insertBefore operation would append that node at the end of the parent container, and the end can be after any other node.
I see. In other words, to differentiate detached element from the last element in container in eg. el.nextSibling === end
case. That problem is mixing of concerns - figuring out node.parentNode === currentParent
and node.nextSibling === null
in a single node.nextSibling === null
check. Btw that's not mandatory for all algos - some do just fine considering end === null
as the "end of container", same as el.insertBefore(child, null)
.
I tried rewriting Dommy for both problems - refs for nextSibling/previousSibling
instead of indexOf
and getting rid of _childNodes
, with little success.
diffing without that ability, in the real-world is kinda useless
I'd rather disagree: diffing all child nodes of a container is what react/preact/morphdom/nanomorph/etc do: render(children, container)
, and the last element is implied null
here, so algos should be just fine without last pin.
I see you fixed that in the #19, great!
Btw that's not mandatory for all algos
I've removed the before
from the equation, it's optionally enabled on top of the file now.
render(children, container)
same goes for uhtml, render(where, what)
, but when it maps a template literal, it needs to know where to work, and never append anything after, example:
render(document.body, html`
<div>
${items.map(item => html`<p>${item.value}</p>`)}
<p>This should stay</p>
</div>
`);
The udomdiff before
element here becomes handy because it will work on this tree:
<div>
<!--pin-->
<p>This should stay</p>
</div>
The list of items can grow and shrink at any time and nothing will ever be appended to that container.
<div>
<p>first</p>
<p>second</p>
<!--pin-->
<p>This should stay</p>
</div>
However, I see that even enabling the before
in the current refactoring, all libraries seems to work just fine, as they use nextSibling
anyway, so they work in a clean way, which is why I am not using before
anymore, but all libraries could receive it, either as undefined/null or as a proper node.