Open creeperyang opened 7 years ago
你好,我想问下关于snabbdom里面的
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
这一部分通过比较头尾节点是否相同从而达到diff比较的算法的具体原理是什么?为什么可以这样做的呢?在算法上有相关的学名吗?
@qiangzi7723 文章里其实已经写了,如果希望更多阅读,可以参考:
为什么一次循环里比较 4 个 vnode?但你可以看到如果 isSameVnode
通过才会处理相应的两个 vnode,然后进入下次循环:
isSameVnode
判断通过说明是新旧队列里的相同的 vnode,当然要做移动/更新处理。isSameVnode
判断都没通过,其实只处理 newStartVnode
,要么从旧队列里找到它对应的 vnode 做更新处理,找不到则当作新插入的 vnode 处理。所以整体逻辑是清晰的。
嗯嗯,感谢,看明白了。发现了个小问题,关于while循环的2⃣️.4小点里面的
// 4. 如果 oldEndVnode 和 newStartVnode 相同,执行 patch
else if (isSameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 这里是左移更新后的 dom,原因参考上面的右移。
api.insertBefore(parentElm, oldEndVnode.elm, api.nextSibling(oldStartVnode.elm))
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
这里面的insertBefore
应该不需要nextSibling
了,直接插到oldStartVnode
前面才对。
@qiangzi7723 👍 非常感谢,的确应该插入到 oldStartVnode.elm
前面。
m
请教下作者,虽然每次对比 4 个 vnode只处理一对 vnode,但是对比4个vnode这样的方法优势在哪里?用比较简单的思路 : 逐个遍历newVdom的节点,找到它在oldVdom中的位置,如果找到了就移动对应的DOM元素,如果没找到说明是新增节点,则新建一个节点插入。遍历完成之后如果oldVdom中还有没处理过的节点,则说明这些节点在newVdom中被删除了,删除它们即可。这样有什么问题吗?我觉得这样算法上反而还简单
@lawpachi 按你的来那时间复杂度就是 O(mn) 了。
再请教作者个问题:我们一般加key几乎都说是为了优化性能。文中也说加key是为了最大化复用已有的 DOM,但是vue官网说v-for采取就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。这个默认的模式是高效的,建议尽可能在使用 v-for 时提供 key,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。所以我理解vue的文档的意思是,判断新旧dom的标签一样采取不移动dom而是更新dom的方式优化性能。加了key需要移动dom,性能会稍差,不知道我的理解是否有问题
看了您贴的 https://zhuanlan.zhihu.com/p/20346379 这篇文章,提到【Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。】,想问下如果使用 React 做组件拖拽,类似搭积木的页面时,React 的 diff 算法会做特别优化吗?
@daweilv 知乎不是我的文章 😂
另外React并不会为特定情况去做优化,想对diff优化,除了用好key/设计好html结构等等,主要是善用 shouldComponentUpdate
,在diff之前拦截掉。
请教作者个问题:我们一般加key几乎都说是为了优化性能。文中也说加key是为了最大化复用已有的 DOM,但是vue官网说v-for采取就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。这个默认的模式是高效的,建议尽可能在使用 v-for 时提供 key,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。所以我理解
我仔细看了下updateChildren , 发现列表对比的时候会先对比首尾,如果都不一样,才会用到key,如果key能再老的数组中找到的话,则是移动,如果找不到,则会创建新的DOM元素。所以增加key是可以获取性能上的提升的
前辈,我看你 /code/snabbdom/* 里面关于webpack 的配置很简单,但是运行您的项目发现竟然有热更新的功能,请问这是为什么呢?是 webpack-dev-server
默认就会开启热更新吗?
前辈,我看你 /code/snabbdom/* 里面关于webpack 的配置很简单,但是运行您的项目发现竟然有热更新的功能,请问这是为什么呢?是
webpack-dev-server
默认就会开启热更新吗?
对
// 比较 old children 和 new children,并更新
if (oldCh && ch) {
if (oldCh !== ch) {
// 核心逻辑(最复杂的地方):怎么比较新旧 children 并更新,对应上面
// 的数组比较
updateChildren(elm, oldCh, ch, insertedVnodeQueue)
}
}
请教一下,这边oldCh 和 ch比对,为什么可以直接用===比较啊,这不是数组吗?
// 比较 old children 和 new children,并更新 if (oldCh && ch) { if (oldCh !== ch) { // 核心逻辑(最复杂的地方):怎么比较新旧 children 并更新,对应上面 // 的数组比较 updateChildren(elm, oldCh, ch, insertedVnodeQueue) } }
请教一下,这边oldCh 和 ch比对,为什么可以直接用===比较啊,这不是数组吗?
正是需要的。如果不是同一个数组,意味着被替换了,不用patch,直接去添加/删除即可。
else if (isSameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
//api.nextSibling(oldEndVnode.elm)这里能取到值吗?
api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}
前辈,这里oldEndVnode
不是最后一个孩子节点吗,他取nextSibling能取到值吗,我看了您的文章,自己实现时发现这里没有值。
```js else if (isSameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) //api.nextSibling(oldEndVnode.elm)这里能取到值吗? api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }
前辈,这里oldEndVnode不是最后一个孩子节点吗,他取nextSibling能取到值吗,我看了您的文章,自己实现时发现这里没有值。
oldEndVnode
指针是不断左移的,不代表一定是老的 vnode 列表的最后一个;oldEndVnode
的 nextSibling
即使为空,也不影响。
oldEndVnode
一次是不断的左移的,不代表一定是老的vnode列表的最后一个;oldEndVnode
的nextSibling
哪怕为空,也不影响。
第一点我理解了,但第二点还是没懂。
如果第一次循环是就是这种情况时, oldEndVnode
是最后一个child,nextSibling
应该是空吧,然后进行insertBefore
,不就会报错吗,我是在insertBefore
中判断了一下为空就append
到父节点,可我看源码也没有这样实现,是如何保证不出错的呢?
伴随 React 兴起, Virtual DOM 也越来越火,各种各样的实现,各个 UI 库的引入等等。snabbdom 就是 Virtual DOM 的一个简洁实现。不过在解读 snabbdom 之前,首先谈一谈 Virtual DOM 。
什么是 Virtual DOM ?
在谈论 Virtual DOM 之前,必须要理解:什么是 DOM ?
DOM 即 Document Object Model,是一种 通过对象表示结构化文档的方式 。DOM 是跨平台的,也是语言无关的(比如 HTML 和 XML 都可以用它表示与操作)。浏览器处理 DOM 的实现细节,然后我们可以通过 JavaScript 和 CSS 来与它交互。
DOM 的主要问题是没有为创建动态 UI 而优化。
以前直接使用 DOM API 比较繁琐,然后有了 jQuery 等库来简化 DOM 操作;但这没有解决大量 DOM 操作的性能问题。大型页面/单页应用里动态创建/销毁 DOM 很频繁(尤其现在前端渲染的普遍),我们当然可以用各种 trick 来优化性能,但这太痛苦了。
而 Virtual DOM 就是解决问题的一种探索。
Virtual DOM 建立在 DOM 之上,是基于 DOM 的一层抽象,实际可理解为用更轻量的纯 JavaScript 对象(树)描述 DOM(树)。
操作 JavaScript 对象当然比操作 DOM 快,因为不用更新屏幕。我们可以随意改变 Virtual DOM ,然后找出改变再更新到 DOM 上。但要保证高效,需要解决以下问题:
带着这些问题,我们进入正题:以 snabbdom 为例,讲讲怎么实现一个 Virtual DOM 库。
snabbdom 总览
snabbdom 的 ES6 改写代码可在 codes/snabbdom 浏览,有
id/className
处理等的小改动,但核心流程完全一致。代码结构:
snabbdom 是轻量的 Virtual DOM 实现,代码量少,模块化,结构清晰。这是我选择 snabbdom 作为源码阅读目标的主要原因。
snabbdom 主要的接口有:
h(type, data, children)
,返回 Virtual DOM 树。patch(oldVnode, newVnode)
,比较新旧 Virtual DOM 树并更新。从两个接口开始,下面我们深入讲解 snabbdom 的实现。
snabbdom 的实现
怎么实现 Virtual DOM ?我们首先要定义 Virtual DOM,或者具体点,定义一个 Virtual DOM 节点:vnode。
vnode.js
vnode 是对 DOM 节点的抽象,既然如此,我们很容易定义它的形式:
对应源代码src/vnode.js:
代码几乎一眼就懂,有三点注意下:
构造 vnode 时内置了
__type
,值为 symbol 。利用 symbol 的唯一性来校验 vnode。vnode 的 children/text 二选一,不可共存。那为什么不把 text 视为 children 的一个元素 ?主要是方便处理,text 节点和其它类型的节点处理起来差异很大。
elm
用于保存 vnode 对应 DOM 节点。isSameVnode
检查两个 vnode 是否 相同。这里的相同是指后一个 vnode 是否由之前的 vnode 变换而来,要求 type 相同且 key 相同:div
变到p
,那么 vnode 对应的 DOM 则必须整个替换掉了;h.js
定义了 vnode 的格式,那么我们可以组合 vnode 得到一颗 Virtual DOM 树了。
h
函数就是来帮我们生成虚拟 DOM 树的。源代码src/h.js:
如上,有一点可以注意:参数
children
既可以是Array
,也可以是vnode
,甚至是字符串。如果是字符串自动转换成 vnode(该 vnode 的 text 即该字符串)。多数情况下,或者说要有更好的开发体验,我们应该支持
JSX
或类似的语法,然后通过 babel 插件转换成h
的函数调用。鉴于本文主题是怎么实现 Virtual DOM,这里就不展开了。index.js
现在进入 snabbdom 的核心,来讲 Virtual DOM 必须实现的两个功能:
从简单的开始,先讲怎么从 vnode 生成 DOM:
上面的代码并不复杂,也不应该复杂,因为 Virtual DOM 是对 DOM 的抽象,是描述,从 Virtual DOM 生成 DOM 本来就应该是直接简明的:根据 type 生成对应的 DOM,把 data 里定义的 各种属性设置到 DOM 上。
当然这里隐藏了一些复杂性,比如 style 处理,比如边缘情况处理等等。
接下来讲怎么 diff 两颗 Virtual DOM 树,并执行最小更新。
通常情况下,找到两棵任意的树之间最小修改的时间复杂度是 O(n^3),这不可接受。幸好,我们可以对 Virtual DOM 树有这样的假设:
如果 oldVnode 和 vnode 不同(如 type 从
div
变到p
,或者key
改变),意味着整个 vnode 被替换(因为我们通常不会去跨层移动 vnode ),所以我们没有必要去比较 vnode 的 子 vnode(children) 了。基于这个假设,我们可以 按照层级分解 树,这大大简化了复杂度,大到接近 O(n) 的复杂度:此外,对于 children (数组)的比较,因为同层是很可能有移动的,顺 序比较会无法最大化复用已有的 DOM。所以我们通过为每个 vnode 加上 key 来追踪这种顺序变动。
原理分析完,上代码:
到这里讲完了 snabbdom 的核心实现,可以发现,Virtual DOM 比我们想的会简单一点。
本篇限于篇幅,代码肯定不会贴全,想看完整代码的可以去官方或者我用 ES6 改写的仓库。
题外话,本篇作为 React学习笔记 系列的第二篇,可结合 #30 一起观看。