暂停/完成的视频在YouTube上当向上滚动到足够加载新帖时会自动开始播放

修补虚拟 DOM 来解决这个问题有什么不对或难的地方吗?

欢迎提交 PR,我们尝试过但实在太难了

2 个赞

很有意思!迁移到 vdom 的一个分支并非理想选择,但只要我们能证明没有回归(并且我们将其迁移到我们自己的 GitHub 组织),我们或许可以考虑。

您是否能够运行 vdom 测试套件来确认没有回归?为这种前置行为添加新的 vdom 测试会容易吗?

2 个赞

我将首先回答第二个问题。这种预置行为基本上是相同的,它只是消除了额外的工作。我将尝试说明 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 视频开始播放,因为最初旧元素被重新插入(这会触发它们被重新渲染),而在修补后它们会保持在原位。

2 个赞

这是 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 的旧版本一致性。

2 个赞

:+1: 请这样做——让我们保持他们现有的测试套件正常运行

我刚刚将 virtual-dom 分叉到 GitHub - discourse/virtual-dom: A Virtual DOM and diffing algorithm - 请向该存储库提交一个 PR?

3 个赞
3 个赞

我还应该指出,我对 virtual-dom 的更改非常有限。它专门针对 prepends,并在所有其他情况下回退到原始算法。而且,如果普遍看待这个问题,原始算法仍然不完美(不难举出一些例子,它会不必要地触及旧元素)。另一方面,它能很好地处理 appends、removes、single inserts。而且有了这个,你需要对 post stream 进行真正的优化才能突破。所以,实际上解决普遍问题可能有点矫枉过正,尽管当然,当你这样做时,你可以睡得更好。

我推送了一些新的测试。你打算近期审查一下这个 PR 吗?

1 个赞

谢谢 Aleksey - 我将在下周内完成审阅

4 个赞

我已经设置了 virtualdom 分支,并且可以确认它解决了手动测试中的 iframe 问题。

@Aleksey_Bogdanov 我想知道你是否能帮我处理一些事情。我一直在尝试在 Discourse 中添加一个测试。本质上:

  1. 渲染 <span>ElementOne</span><span>ElementTwo</span>

  2. 删除 ElementTwo,并预置 PrependedElement

  3. 检查结果是否为 <span>PrependedElement</span><span>ElementOne</span>

  4. 检查原始 ElementOne span 是否等于最终的 ElementOne span(即检查它没有被重新渲染)

不幸的是,(4)中的检查失败了,这意味着 ElementOne 被重新渲染了。你有什么想法为什么新的逻辑在这种情况下不起作用吗?:thinking:

我已经将新的分支配置和失败的测试推送到现有的 PR

3 个赞

是的,我之前写过关于这个问题
我不会期望这能普遍奏效(并且你描述的测试能通过)。最初,它适用于追加、删除、单个插入(也许是它们的某种组合)。通过我的更改,它也适用于纯粹的前置,这应该能解决 YouTube 的 bug。你的测试包含“删除 + 前置”。这不在我的更改范围内。
一个更健壮的解决方案至少需要重写 virtual-dom 中的 ‘reorder’ 和 ‘diffChildren’。我可以尝试一下,但这引出了一个问题:你打算长期维护自己的 virtual-dom 分支吗?如果我们想做到健壮,尝试切换到一个类似的主动维护的库可能更好,并且花费的时间和精力更少。我猜其他库现在已经解决了这个问题。

2 个赞

我明白了——这说得通。但是,即使我将其改为“纯前插”,我也会遇到同样的失败。在相关的 reorder() 函数中设置断点,可以看到它在 bFree.length === bChildren.length 检查时出错:

我已经将更新后的测试推送到分支了。

这绝对只是一个短期解决方案。我们已经开始用 Ember/Glimmer 渲染替换我们对 vdom 的使用。我们的目标是在未来 12 个月内替换帖子流实现。Glimmer 可以正确处理这种“前插”。

所以,我很乐意合并一个“不完整但比以前更好”的更改。但是,如果这不涉及太多工作,我想了解为什么这个测试不起作用 :thinking:

3 个赞

当然。我还不确定,但我会尽快查看。

3 个赞

更新。
我添加了密钥以使测试通过。密钥会提示 virtual-dom 哪些元素对应哪些元素。没有它们,virtual-dom 就会假设元素遵循与之前相同的顺序,并且无法弄清楚它正在处理前置操作。
然后,我将单个前置更改为多个前置,因为 virtual-dom 已经涵盖了单个插入(单个前置是一种特殊情况),所以它们不应该有区别。
以上更改是我目前已推送的内容。

但是现在多个前置操作没有区别,这令人惊讶。我最初关于问题所在以及我的补丁为何有效的理论可能存在缺陷。所以我还在努力弄清楚。
我注意到,David,你在浏览器中调试了测试。设置起来困难吗?我很难为测试设置调试器,如果你能给我一些指导,我将不胜感激。

3 个赞

[quote=“Aleksey Bogdanov, post:59, topic:57692, username:Aleksey_Bogdanov”]
我添加了用于测试通过的键。键提示虚拟 DOM 哪些元素对应哪些元素
[/quote]啊,我明白了——谢谢!

你有一个可用的 Discourse 开发设置吗?如果有,请启动它,在浏览器中访问 /tests,然后使用顶部的过滤器 UI 搜索“avoids rerendering on prepend”。然后你可以使用浏览器开发者工具进行调试。(例如,转到 sources 选项卡,按 Ctrl+P 打开文件,搜索 vdom/diff,然后设置一个断点)

3 个赞

谢谢,我会试试的。

3 个赞

所以,这段代码

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 变异检查。现在之前的版本测试失败,而当前版本测试通过,所以希望这足够了。

6 个赞

太好了,非常感谢 @Aleksey_Bogdanov 的工作。我刚刚合并了拉取请求,它将在未来一小时内在 Meta 上线。

6 个赞