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(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { |
四、总结
- 正确使用key,可以快速执行sameVnode比对,加速Diff效率,可以作为性能优化的一个点。
- DIff只做同级比较,使用sameVnode函数比对,文本节点直接替换文本内容。
- 子元素列表的Diff,进行头对头、尾对尾、头对尾等系列比较,直到遍历完两个元素的子元素列表。
- 或一个列表先遍历完了,直接addVnode / removeVnode。
掘金:前端LeBron
知乎:前端LeBron
持续分享技术博文,关注微信公众号👇🏻