vue源码图解03-diff(patch)算法

695 阅读5分钟

虚拟DOM概念

虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用 的各种状态变化会作用于虚拟DOM,最终映射到DOM上。

vue 1.x中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大 型项目来说是不可接受的。因此,vue 2.x选择了中等粒度的解决方案,每一个组件一个watcher实例, 这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。

我们知道,render.js是生成vnode,而lifecycle.js中,update方法是负责更新dom,将vnode转换为真实dom,我们来看看lifecycle.js。

instance/lifecycle.js

lifecycleMixin.jpg 我们看到,核心在于vm.__patch__方法,那么这个方法又是怎么来的呢?

这个要回溯到最开始的时候,初始化时,在runtime/index.js中,我们再去看看做了什么事。

runtime/index.js

patch.png patch函数哪儿来的呢?在同目录下,有一个patch.js文件。

runtime/patch.js

createPatchFunction.jpg

patch函数是工程函数createPatchFunction的返回值,传递的nodeOps和modules是web平台的实现。

很清晰地看到,createPatchFunction函数是从vdom/patch.js文件来的,这也是我们要学习patch的核心文件。

vdom/patch.js

在文件的开头,我们看到作者说的,由于性能的考虑,patch文件没有使用flow来写。而且需要提一下,因为是一棵DOM树的比较,所以就有了三种情况:增删改。

patch图.jpg

  • 旧节点不存在,那么就新增
  • 新节点不存在,那么就删除
  • 新旧节点都存在,那么就执行diff算法(patch算法)来更新

patch方法

patch.jpg

那么我们再去看看patchVnode方法,去看看diff发生的真正的地方。

patchVnode方法

在patchVnode方法中,最开始做了一些特殊情况处理,也就是异步组件及静态节点(不需要比对的)处理,然后就是核心代码:

patchVnode.png

我们可以看到,比较两个vnode,有三种类型操作:属性更新、子节点更新、文本更新。

规则:

  1. 新旧节点都有子节点,那么对子节点进行diff算法,调用updateChildren方法。
  2. 如果新节点有子节点,而旧节点没有子节点,先清空旧节点的文本内容,然后为其新增子节点。
  3. 如果新节点没有子节点,而旧节点有子节点,则移除该节点的所有子节点。
  4. 当新旧节点都没有子节点时,那么就只是文本的替换。

因为是一棵DOM树的比较,所以其实最多的是都有子节点,也就是说重排updateChildren是最最核心的方法。

updateChildren方法

updateChildren主要作用是用一种较高效的方式比对新旧两个vnode的子节点得出最小操作补丁。执行一个双循环来一个一个比较是传统的方式,但是结合我们使用场景,vue做了特别的算法优化,因为根据统计,我们可以知道,两个子节点发生非常大变化的情况比较少,更多的是首尾增加元素、首尾删除元素、倒序等,在这些情况下,首尾总能找到相同的节点,那么就根本不需要循环。那么vue是怎么做的呢?

首先在新旧节点首尾都做一个游标(变量标记),记录游标的位置,然后开始两两交叉比较。在比较过程中这几个游标都会向中间靠拢,当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束。

图解:

updateChildren.jpg

源码:

游标.jpg

规则1

当 oldStartVnode和newStartVnode 相同的时候(满足sameVnode),直接将该VNode节点进行patchVnode,并将两个游标向中间移动一个,不需再遍历就完成了一次循环。

新旧首相同.jpg

旧首新尾相同1.jpg

规则2

当oldEndVnode和newEndVnode 相同的时候(满足sameVnode),直接将该VNode节点进行patchVnode,并将两个游标向中间移动一个,不需再遍历就完成了一次循环。

新旧尾相同.jpg

新旧尾相同1.jpg

规则3

如果oldStartVnode与newEndVnode满足sameVnode。说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将oldStartVnode移动到oldEndVnode的后面,并将两个游标向中间移动一个。

旧首新尾相同.jpg

旧首新尾相同1.jpg

规则4

如果oldEndVnode与newStartVnode满足sameVnode,说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时要将oldEndVnode移动到oldStartVnode的前面,并将两个游标向中间移动一个。 旧尾新首相同.jpg

旧尾新首相同1.jpg

规则5

如果以上情况均不符合,则在旧节点中找与newStartVnode相同的(满足sameVnode)节点。

  1. 若存在执行patchVnode,同时将找到的节点对应DOM移动到oldStartVnode的前面。 旧中新首相同.jpg
  2. 如果不存在,这个时候会调用createElm创建一个新的DOM节点,插入到oldStartVnode的前面。!

新首旧中没有.jpg

源码:

其他情况.jpg

规则6

循环结束时,我们需要处理剩下的节点。

当结束时oldStartIdx > oldEndIdx,这个时候旧的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,需要将剩下的VNode对应的DOM插入到真实DOM中,此时调用addVnodes(批量调用createElm接口)。

收尾1.jpg

当结束时newStartIdx > newEndIdx时,说明新的VNode节点已经遍历完了,但是老的节点还有剩余,执行批量删除。

收尾2.jpg

源码:

收尾.jpg

补充

1

sameVnode.jpg

2

递归比较的时候,是深度优先。

3

补充.jpg