修补虚拟 DOM 来解决这个问题有什么不对或难的地方吗?
欢迎提交 PR,我们尝试过但实在太难了
很有意思!迁移到 vdom 的一个分支并非理想选择,但只要我们能证明没有回归(并且我们将其迁移到我们自己的 GitHub 组织),我们或许可以考虑。
您是否能够运行 vdom 测试套件来确认没有回归?为这种前置行为添加新的 vdom 测试会容易吗?
我将首先回答第二个问题。这种预置行为基本上是相同的,它只是消除了额外的工作。我将尝试说明 vdom 最初是如何工作的,以及修补后的工作方式。
假设有 20 篇帖子,滚动时会加载另外 10 篇。
最初 vdom 会进行以下一系列转换:
[20 旧] → [20 旧, 10 新] // 10 个新元素被追加到末尾
[20 旧, 10 新] → // 所有 30 个元素都被移除
→ [10 新, 20 旧] // 所有 30 个元素都被插入到它们的位置
修补后 序列变为:
[20 旧] → [20 旧, 10 新] // 10 个新元素被追加到末尾
[20 旧, 10 新] → [20 旧] // 10 个新元素从末尾移除
[20 旧] → [10 新, 20 旧] // 10 个新元素被预置
正如你所见,仍然存在额外的工作:原则上你可以立即预置这些元素,但这样我就可以在一个地方添加一些代码,而不必重写已经存在的内容。
每次转换都成为一次 dom 操作,因此 YouTube 视频开始播放,因为最初旧元素被重新插入(这会触发它们被重新渲染),而在修补后它们会保持在原位。
这是 virtual-dom 的 package.json 的摘录
"scripts": {
"test": "node ./test/index.js | tap-spec",
"dot": "node ./test/index.js | tap-dot",
"start": "node ./index.js",
"cover": "istanbul cover --report html --print detail ./test/index.js",
"view-cover": "istanbul report html & opn ./coverage/index.html",
"browser": "run-browser test/index.js",
"phantom": "run-browser test/index.js -b | tap-spec",
"dist": "browserify --standalone virtual-dom index.js > dist/virtual-dom.js",
"travis-test": "npm run phantom && npm run cover && istanbul report lcov && ((cat coverage/lcov.info | coveralls) || exit 0)",
"release": "npm run release-patch",
"release-patch": "git checkout master && npm version patch && git push origin master --tags && npm publish",
"release-minor": "git checkout master && npm version minor && git push origin master --tags && npm publish",
"release-major": "git checkout master && npm version major && git push origin master --tags && npm publish"
},
在这些脚本中,“test”、“dot”、“cover”、“view-cover”、“browser”、“phantom”、“travis-test”似乎与测试相关。
“browser”、“phantom”、“travis-test”由于我代码中较新的 JavaScript 构造而导致解析错误。其他脚本则通过。如果我将此代码
var prepend = simulate.every(item => item && item.key)
prepend &= aChildren.every((item, i) => item.key === bChildren[i + shift].key)
更改为此代码
var prepend = true
for (var i = 0; prepend && i < simulate.length; i++) {
prepend = simulate[i] && simulate[i].key
}
for (var i = 0; prepend && i < aChildren.length; i++) {
prepend = aChildren[i].key === bChildren[i + shift].key
}
那么所有脚本都能通过。如果希望满足所有这些脚本,我可以将此更改推送到以保持 JavaScript 的旧版本一致性。
请这样做——让我们保持他们现有的测试套件正常运行
我刚刚将 virtual-dom 分叉到 GitHub - discourse/virtual-dom: A Virtual DOM and diffing algorithm - 请向该存储库提交一个 PR?
我还应该指出,我对 virtual-dom 的更改非常有限。它专门针对 prepends,并在所有其他情况下回退到原始算法。而且,如果普遍看待这个问题,原始算法仍然不完美(不难举出一些例子,它会不必要地触及旧元素)。另一方面,它能很好地处理 appends、removes、single inserts。而且有了这个,你需要对 post stream 进行真正的优化才能突破。所以,实际上解决普遍问题可能有点矫枉过正,尽管当然,当你这样做时,你可以睡得更好。
我推送了一些新的测试。你打算近期审查一下这个 PR 吗?
谢谢 Aleksey - 我将在下周内完成审阅
我已经设置了 virtualdom 分支,并且可以确认它解决了手动测试中的 iframe 问题。
@Aleksey_Bogdanov 我想知道你是否能帮我处理一些事情。我一直在尝试在 Discourse 中添加一个测试。本质上:
-
渲染
<span>ElementOne</span><span>ElementTwo</span> -
删除 ElementTwo,并预置
PrependedElement -
检查结果是否为
<span>PrependedElement</span><span>ElementOne</span> -
检查原始 ElementOne span 是否等于最终的 ElementOne span(即检查它没有被重新渲染)
不幸的是,(4)中的检查失败了,这意味着 ElementOne 被重新渲染了。你有什么想法为什么新的逻辑在这种情况下不起作用吗?![]()
我已经将新的分支配置和失败的测试推送到现有的 PR。
是的,我之前写过关于这个问题。
我不会期望这能普遍奏效(并且你描述的测试能通过)。最初,它适用于追加、删除、单个插入(也许是它们的某种组合)。通过我的更改,它也适用于纯粹的前置,这应该能解决 YouTube 的 bug。你的测试包含“删除 + 前置”。这不在我的更改范围内。
一个更健壮的解决方案至少需要重写 virtual-dom 中的 ‘reorder’ 和 ‘diffChildren’。我可以尝试一下,但这引出了一个问题:你打算长期维护自己的 virtual-dom 分支吗?如果我们想做到健壮,尝试切换到一个类似的主动维护的库可能更好,并且花费的时间和精力更少。我猜其他库现在已经解决了这个问题。
我明白了——这说得通。但是,即使我将其改为“纯前插”,我也会遇到同样的失败。在相关的 reorder() 函数中设置断点,可以看到它在 bFree.length === bChildren.length 检查时出错:
我已经将更新后的测试推送到分支了。
这绝对只是一个短期解决方案。我们已经开始用 Ember/Glimmer 渲染替换我们对 vdom 的使用。我们的目标是在未来 12 个月内替换帖子流实现。Glimmer 可以正确处理这种“前插”。
所以,我很乐意合并一个“不完整但比以前更好”的更改。但是,如果这不涉及太多工作,我想了解为什么这个测试不起作用 ![]()
当然。我还不确定,但我会尽快查看。
更新。
我添加了密钥以使测试通过。密钥会提示 virtual-dom 哪些元素对应哪些元素。没有它们,virtual-dom 就会假设元素遵循与之前相同的顺序,并且无法弄清楚它正在处理前置操作。
然后,我将单个前置更改为多个前置,因为 virtual-dom 已经涵盖了单个插入(单个前置是一种特殊情况),所以它们不应该有区别。
以上更改是我目前已推送的内容。
但是现在多个前置操作没有区别,这令人惊讶。我最初关于问题所在以及我的补丁为何有效的理论可能存在缺陷。所以我还在努力弄清楚。
我注意到,David,你在浏览器中调试了测试。设置起来困难吗?我很难为测试设置调试器,如果你能给我一些指导,我将不胜感激。
[quote=“Aleksey Bogdanov, post:59, topic:57692, username:Aleksey_Bogdanov”]
我添加了用于测试通过的键。键提示虚拟 DOM 哪些元素对应哪些元素
[/quote]啊,我明白了——谢谢!
你有一个可用的 Discourse 开发设置吗?如果有,请启动它,在浏览器中访问 /tests,然后使用顶部的过滤器 UI 搜索“avoids rerendering on prepend”。然后你可以使用浏览器开发者工具进行调试。(例如,转到 sources 选项卡,按 Ctrl+P 打开文件,搜索 vdom/diff,然后设置一个断点)
谢谢,我会试试的。
所以,这段代码
assert.strictEqual(elementOneBefore, elementOneAfter);
没有区别,因为无论如何,elementOneBefore 在所有这些更改中都保持其身份。
如果您想亲自看看,这里有一个小演示:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Youtube Bug Demo</title>
</head>
<body>
<script>
let iframe = document.createElement("iframe");
iframe.width = 690;
iframe.height = 388;
iframe.src = "https://www.youtube.com/embed/Xc5rB-0ZBcI";
iframe.title = "Strange S.T.A.L.K.E.R. car glitch";
document.body.appendChild(iframe);
let button = document.createElement("button");
button.type = "button";
button.innerHTML = "Reinsert";
button.onclick = function(){
let iframeStart = document.querySelector("iframe");
document.body.removeChild(iframeStart);
document.body.insertBefore(iframeStart, button);
let iframeEnd = document.querySelector("iframe");
alert(iframeStart === iframeEnd);
};
document.body.appendChild(button);
</script>
</body>
</html>
有区别的是那些 ‘removeChild’、‘insertBefore’ 调用,所以我添加了一个 DOM 变异检查。现在之前的版本测试失败,而当前版本测试通过,所以希望这足够了。
太好了,非常感谢 @Aleksey_Bogdanov 的工作。我刚刚合并了拉取请求,它将在未来一小时内在 Meta 上线。
