Vue(七):diff 算法

140 阅读4分钟

diff 算法是什么

diff 算法是一种通过同层树节点进行比较的高效算法,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较,两个特点:

  • 比较只会在同层级进行,不会跨层级比较

  • 在 diff 比较的过程中,循环从两边向中间比较,即双端 diff 算法

什么是虚拟 DOM

[ˈvɜːtʃuəl]

Virtual DOM 实际上是对真实 DOM 的抽象,是以 Javascript 对象作为基础的树,用对象的属性来描述节点,对象至少包含标签名(tag)、属性(attrs)和子元素对象(children)三个属性,最终通过一系列操作使得这棵树映射到真实环境上。

例如:

<div>
    <p>123</p>
</div>

抽象成 Virtual DOM 就是:

var VNode = {
    tag: 'div',
    children: [
        { tag: 'p', text: '123' }
    ]
};

为什么使用虚拟 DOM

渲染真实 DOM 的开销是很大的,页面的性能问题,大部分都是由 DOM 引起的。操作原生 DOM ,浏览器会从构建 DOM 树开始从头到尾执行一遍流程。当一次操作需要更新 10 个 DOM 节点,浏览器没这么智能,收到第一个更新 DOM 请求后,并不知道后续还有 9 次更新操作,会马上执行流程,最终执行 10 次流程。

而通过 VNode,同样更新 10 个 DOM 节点,虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存到的一个 Javascript 对象中,最终将这个 Javascript 对象一次性 attach 到 DOM 树上,避免大量的无谓计算。

很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 带来的性能消耗。虽然这虚拟 DOM 的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,它不仅仅局限于浏览器的 DOM,也可以是安卓和 IOS 的原生组件,也可以是小程序。

双端 diff 算法策略

双端 diff 算法策略:四种命中查找,四个指针,旧新对比。

  1. 头对头:如果匹配, 旧前指针++ 新前指针++,

  2. 尾对尾:如果匹配,旧后指针--, 新后指针--,

  3. 头与尾:如果匹配,旧前指针++, 新后指针--,

  4. 尾与头:如果匹配,旧后指针--, 新前指针++,

  5. 都不满足,需要查找:如果查到,新的指针++, 新的添加页面的数据替换

image.png

为什么推荐使用 key

key 的作用主要是为了高效地更新虚拟 DOM。key 主要用来对比两个虚拟 DOM 节点时,判断其是否为相同节点。如果 key 一样就直接对比其 children 中不同就行, 如果没有 key 值,就需要对比其他很多属性参数(很多很多)才能得出是不是相同的节点。

核心源码理解

当数据发生改变时,set 方法会调用 Dep.notify 通知所有订阅者 watcher,订阅者就会调用 patch 给真实 DOM 打补丁,更新对应的视图。

patch 前两个参数为 oldVnode 和 Vnode,分别代表旧节点和新节点,主要做了四个判断:

  1. 没有新节点,直接触发旧节点的 destory 钩子

  2. 没有旧节点,说明页面是刚开始初始化时候,此时,不需要比较,直接新建,只调用 createRealElement

  3. 旧节点和新节点自身一样,通过 sameVnode 判断节点是否一样,一样时,调用 patchVnode 去处理这两个节点

  4. 旧节点和新节点自身不一样,直接创建新节点,删除旧节点

patchVnode 主要做了几个判断:

  1. 新节点是否是文本节点,如果是,直接更新 DOM 的文本内容为新节点的文本内容

  2. 新旧节点都有子节点的情况下,则处理比较更新子节点(diff 算法核心)

  3. 只有新节点有子节点,旧节点没有,那么不用比较,所有节点都是全新的,直接新建 DOM,添加进父节点中

  4. 只有旧节点有子节点,说明更新后的页面,旧节点全部不见了,所以,把所有的旧节点删除,也就是直接把 DOM 删除

vue2 diff 的痛点

vue2.x 中的虚拟 DOM 是进行全量的对比,在运行时会对所有节点生成一个虚拟节点树,当页面数据发生变更好,会遍历判断 virtual DOM 所有节点(包括一些不会变化的节点)有没有发生变化;虽然说 diff 算法确实减少了多 DOM 节点的直接操作,但是这个「减少是有成本的」,如果是复杂的大型项目,必然存在很复杂的父子关系的VNode,而 Vue2.x 的 diff 算法,会不断地递归调用 patchVNode,不断堆叠而成的几毫秒,最终就会造成 VNode 更新缓慢

Vue3.x 对于不参与更新的元素,做静态标记并提示,只会被创建一次,在渲染时直接复用。

原文链接