-
Notifications
You must be signed in to change notification settings - Fork 211
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
解析 snabbdom 源码,教你实现精简的 Virtual DOM 库 #33
Comments
你好,我想问下关于snabbdom里面的
这一部分通过比较头尾节点是否相同从而达到diff比较的算法的具体原理是什么?为什么可以这样做的呢?在算法上有相关的学名吗? |
@qiangzi7723 文章里其实已经写了,如果希望更多阅读,可以参考: 为什么一次循环里比较 4 个 vnode?但你可以看到如果
所以整体逻辑是清晰的。 |
嗯嗯,感谢,看明白了。发现了个小问题,关于while循环的2⃣️.4小点里面的
这里面的 |
@qiangzi7723 👍 非常感谢,的确应该插入到 |
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结构等等,主要是善用 |
我仔细看了下updateChildren , 发现列表对比的时候会先对比首尾,如果都不一样,才会用到key,如果key能再老的数组中找到的话,则是移动,如果找不到,则会创建新的DOM元素。所以增加key是可以获取性能上的提升的 |
写了一篇深度解读 |
前辈,我看你 /code/snabbdom/* 里面关于webpack 的配置很简单,但是运行您的项目发现竟然有热更新的功能,请问这是为什么呢?是 |
对 |
// 比较 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]
} 前辈,这里 |
|
第一点我理解了,但第二点还是没懂。 |
伴随 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 一起观看。
The text was updated successfully, but these errors were encountered: