计算两颗树的结构差异并进行转化,传统的diff算法是通过循环递归对每个节点进行依次对比,算法复杂度达到 O(n^3) ,n是树的节点数,这个有多可怕呢?——如果要展示1000个节点,得执行上亿次比较。。即便是CPU快能执行30亿条命令,也很难在一秒内计算出差异。
最开始经典的深度优先遍历算法,其复杂度为O(n^3),存在高昂的diff成本。然后cito.js的横空出世,提出两项优化方案,使用key实现移动追踪及基于key的编辑长度距离算法应用(算法复杂度 为O(n^2))。但这样的diff算法太过复杂了,于是后来者snabbdom将kivi.js进行简化,去掉编辑长度距离算法,调整两端比较算法。速度略有损失,但可读性大大提高。再之后,就是著名的Vue2.0 把snabbdom整个库整合掉了。
所以如果觉得直接看Vue的diff算法太多干扰,可以直接对照着snabbdom,方便梳理Vue diff的核心代码。本文从组件Watcher中触发updateComponent开始讲解,每段代码前都会附带基于Vue v2.6.11版本的源码Github地址。
Watcher触发diff
当Watcher监听到data发生变化后,触发updateComponent,调用vm._render()生成新的vnode树,传入vm._update中。
1 | // https://github.com/vuejs/vue/blob/v2.6.11/src/core/instance/lifecycle.js#L197 |
1 | // https://github.com/vuejs/vue/blob/v2.6.11/src/core/instance/lifecycle.js#L189 |
Vue原型上的_update方法将当前vm上保存着上一次render的vnode树,将新render的vnode和oldVnode传入patch中,就开始diff了。
1 | // https://github.com/vuejs/vue/blob/v2.6.11/src/core/instance/lifecycle.js#L59 |
__patch__方法是由src/core/vdom/patch下的createPatchFunction方法返回的一个闭包函数的,然后赋值到Vue的原型上的,
1 | // https://github.com/vuejs/vue/blob/v2.6.11/src/platforms/web/runtime/patch.js#L12 |
1 | // https://github.com/vuejs/vue/blob/v2.6.11/src/platforms/web/runtime/index.js#L34 |
执行patch
- 如果
oldVnode不为空,vnode为空,则直接销毁oldVnode的真实DOM - 如果
oldVnode为空,vnode不为空,则为vnode创建一个新的真实DOM - 如果
oldVnode和vnode是相同节点,执行patchVnode(oldVnode, vnode) - 如果
oldVnode和vnode不是相同节点,删除oldVnode,创建新的vnode并插入
1 | // https://github.com/vuejs/vue/blob/v2.6.11/src/core/vdom/patch.js#L70 |
patchVnode
到了patchVnode方法这里,就正式开始进行递归diff了,这个方法会根据oldVnode和vnodedata的差异,更新对应的真实DOM,并且对真实DOM的children继续执行patchVnode递归更新。
1 | // https://github.com/vuejs/vue/blob/v2.6.11/src/core/vdom/patch.js#L501 |
更新真实DOM的方法存在了cbs变量中,是由createPatchFunction方法调用的时候,根据传入的modules生成的。这里是为了不同平台使用不同的更新真实DOM方式,因此modules可以由src/platforms/web/runtime/modules/index.js或src/platforms/weex/runtime/modules/index.js导出,分别用于支持web环境或weex环境。
modules会导出形如下面这样的格式
1 | // https://github.com/vuejs/vue/blob/v2.6.11/src/platforms/web/runtime/modules/attrs.js#L116 |
1 | // https://github.com/vuejs/vue/blob/v2.6.11/src/core/vdom/patch.js#L33 |
1 | // https://github.com/vuejs/vue/blob/v2.6.11/src/core/vdom/patch.js#L70 |
updateChildren
updateChildren方法会对传入的oldCh和newCh进行启发式diff,算法里对几种最常见的vnode变更情况做了优化:
- oldCh和newCh的头和头进行比较
- oldCh和newCh的尾和尾进行比较
- oldCh和newCh的头和尾进行比较
- oldCh和newCh的尾和头进行比较
oldStartIdx、oldEndIdx、newStartIdx、newEndIdx保存着当前比较的vnode的下标,头部比较成功后,idx就往尾部移动,尾部比较成功后,idx头部移动。
当oldCh头部和newCh尾部比较成功后,则将oldCh尾部的节点移动插入到newCh的头部,oldCh尾部和newCh头部比较成功后,也是一样将oldCh头部的节点移动插入到newCh的尾部。
当循环结束的时候,则可能是oldStartIdx > oldEndIdx(oldCh已经被遍历完了)或者newStartIdx <= newEndIdx(newCh已经被遍历完了),如果是oldCh被遍历完,而newCh没有被遍历完,则说明newCh中剩下的是新插入的节点,如果是newCh被遍历完,而oldCh没有被遍历完,则说明oldCh中剩下的是被移除的节点。
1 | // https://github.com/vuejs/vue/blob/v2.6.11/src/core/vdom/patch.js#L404 |
递归执行完patchVnode和updateChildren之后,整棵vnode树就被完全更新了,diff结束。
完整流程图