Vue进阶 Diff算法详解 | 学习笔记
一、虚拟 DOM
什么是虚拟 DOM?
虚拟 DOM 就是把真实 DOM 树的结构和信息抽象出来,以对象的形式模拟树形结构,如下:
真实 DOM:
<div> |
对应的虚拟 DOM 就是:
let vnode = { |
为什么需要虚拟 DOM?
渲染真实 DOM 会有一定的开销,如果每次修改数据都进行真实 DOM 渲染,都会引起 DOM 树的重绘和重排,性能开销很大。那么有没有可能只修改一小部分数据而不渲染整个 DOM 呢?虚拟 DOM 和 Diff 算法可以实现。
怎么实现?
- 先根据真实 DOM 生成一颗虚拟 DOM 树
- 当某个 DOM 节点数据发生改变时,生成一个新的 Vnode
- 新的 Vnode 和旧的 oldVnode 进行对比
- 通过 patch 函数一边比对一边给真实 DOM 打补丁或者创建 Vnode、移除 oldVnode 等
有什么不一样?
- 真实 DOM 操作为一个属性一个属性去修改,开销较大。
- 虚拟 DOM 直接修改整个 DOM 节点再替换真实 DOM
还有什么好处?
Vue 的虚拟 DOM 数据更新机制是异步更新队列,并不是数据变更马上更新 DOM,而是被推进一个数据更新异步队列统一更新。想要马上拿到 DOM 更新后 DOM 信息?有个 API 叫 Vue.nextTick
二、 Diff 算法
传统 Diff 算法
遍历两棵树中的每一个节点,每两个节点之间都要做一次比较。
比如 a->e 、a->d 、a->b、a->c、a->a
- 遍历完成的时间复杂度达到了 O(n^2)
- 对比完差异后还要计算最小转换方式,实现后复杂度来到了 O(n^3)
Vue 优化的 Diff 算法
Vue 的 diff 算法只会比较同层级的元素,不进行跨层级比较
三、 Vue 中的 Diff 算法实现
Vnode 分类
- EmptyVNode: 没有内容的注释节点
- TextVNode: 文本节点
- ElementVNode: 普通元素节点
- ComponentVNode: 组件节点
- CloneVNode: 克隆节点,可以是以上任意类型的节点,唯一的区别在于 isCloned 属性为 true
Patch 函数
patch 函数接收以下参数:
- oldVnode:旧的虚拟节点
- Vnode:新的虚拟节点
- hydrating:是否要和真实 DOM 混合
- removeOnly:特殊的 flag,用于 transition-group
处理流程大致分为以下步骤:
- vnode 不存在,oldVnode 存在时,移除 oldVnode
- vnode 存在,oldVnode 不存在时,创建 vnode
- vnode 和 oldVnode 都存在时
- 如果 vnode 和 oldVnode 是同一个节点(通过 sameVnode 函数对比 后续详解),通过 patchVnode 进行后续比对工作
- 如果 vnode 和 oldVnode 不是同一个节点,那么根据 vnode 创建新的元素并挂载至 oldVnode 父元素下。如果组件根节点被替换,遍历更新父节点 element。然后移除旧节点。如果 oldVnode 是服务端渲染元素节点,需要用 hydrate 函数将虚拟 dom 和真是 dom 进行映射
源码如下,已写好注释便于阅读
return function patch(oldVnode, vnode, hydrating, removeOnly) { |
sameVnode 函数
Vue 怎么判断是不是同一个节点?流程如下:
- 判断 Key 值是否一样
- tag 的值是否一样
- isComment,这个不用太关注。
- 数据一样
- sameInputType(),专门对表单输入项进行判断的:input 一样但是里面的 type 不一样算不同的 inputType
从这里可以看出 key 对 diff 算法的辅助作用,可以快速定位是否为同一个元素,必须保证唯一性。
如果你用的是 index 作为 key,每次打乱顺序 key 都会改变,导致这种判断失效,降低了 Diff 的效率。
因此,用好 key 也是 Vue 性能优化的一种方式。
- 源码如下:
function sameVnode(a, b) { |
patchVnode 函数
前置条件 vnode 和 oldVnode 是同一个节点
执行流程:
- 如果 oldVnode 和 vnode 引用一致,可以认为没有变化,return
- 如果 oldVnode 的 isAsyncPlaceholder 属性为 true,跳过检查异步组件,return
- 如果 oldVnode 跟 vnode 都是静态节点,且具有相同的 key,同时 vnode 是克隆节点或者 v-once 指令控制的节点时,只需要把 oldVnode.elm 和 oldVnode.child 都复制到 vnode 上,也不用再有其他操作,return
- 如果 vnode 不是文本节或注释节点
- 如果 vnode 和 oldVnode 都有子节点并且两者子节点不一致时,就调用 updateChildren 更新子节点
- 如果只有 vnode 有自子节点,则调用 addVnodes 创建子节点
- 如果只有 oldVnode 有子节点,则调用 removeVnodes 把这些子节点都删除
- 如果 vnode 文本为 undefined,则清空 vnode.elm 文本
- 如果 vnode 是文本节点但是和 oldVnode 文本内容不同,只需更新文本。
源代码如下,已写好注释便于阅读
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) { |
updateChildren 函数
重点!!!
前置条件:vnode 和 oldVnode 的 children 不相等
整体的执行思路如下:
-
vnode 头对比 oldVnode 头
-
vnode 尾对比 oldVnode 尾
-
vnode 头对比 oldVnode 尾
-
vnode 尾对比 oldVnode 头
- 只要符合一种情况就进行 patch,移动节点,移动下标等操作
-
都不对再在 oldChild 中找一个 key 和 newStart 相同的节点
-
找不到,新建一个。
-
找到,获取这个节点,判断它和 newStartVnode 是不是同一个节点
- 如果是相同节点,进行 patch 然后将这个节点插入到 oldStart 之前,newStart 下标继续移动
- 如果不是相同节点,需要执行 createElm 创建新元素
-
为什么会有头对尾、尾对头的操作?
- 可以快速检测出 reverse 操作,加快 diff 效率。
源码如下 已写好注释便于阅读:
function updateChildren( |
四、总结
- 正确使用 key,可以快速执行 sameVnode 比对,加速 Diff 效率,可以作为性能优化的一个点。
- DIff 只做同级比较,使用 sameVnode 函数比对,文本节点直接替换文本内容。
- 子元素列表的 Diff,进行头对头、尾对尾、头对尾等系列比较,直到遍历完两个元素的子元素列表。
- 或一个列表先遍历完了,直接 addVnode / removeVnode。
掘金:前端 LeBron
知乎:前端 LeBron
持续分享技术博文,关注微信公众号 👇🏻