详解diff 算法

402 阅读5分钟

详解diff 算法

1、 介绍和认识

Diff算法(差异算法)是一种用于比较两个文本或数据结构之间差异的算法。它主要用于计算出两个版本之间的增量更新,以便在更新时只传输或应用发生变化的部分,而不是整个数据或文本。Diff算法在软件开发、版本控制、文本处理等领域有广泛的应用。

diff 算法是渲染器中最重要的部分,用通俗一些的话来说,其实就是比较变化了哪些部分呗,应用到前端其实就是dom结构层部分的变化。

👉 这里我们主要是通过React和Vue在diff算法的应用方面简单来聊一聊,React和Vue都是基于vdom的框架,dom渲染过程如下:

  • 组件渲染(返回 vdom)=>渲染器 (对比更改vdom 同步到 dom) 在dom变化的时候都是对比vdom
  • 再次渲染 (产生新的 vdom)=> 渲染器(对比新旧 vdom 树)=> 更改部分更新到dom

👉 这里对比两棵vdom 树,找到有差异的部分的算法,就叫做 diff 算法,采用图的方式我们可以来看一下,这里我简单画了一个辅助我们理解。

image.png

- 作用

看完了diff算法的简单认识,那么在实际工作和框架场景之中对于采用的diff算法有什么作用呢

提升性能: diff算法最重要的就是在优化视图更新过程中进行使用的。通过比较新旧虚拟 DOM 树差异,可以精准地计算出需要更新的部分,从而避免不必要的 DOM 操作,大幅提高性能。

2、vue2的diff算法原理

vue2的diff算法原理

Vue2的 diff 算法基于 虚拟 DOM,本质上其实就是利用逐层比较(same-node diffing)和树形结构 diffing来优化性能的

(1)逐层比较

Vue在 diff 算法中逐层对比新旧虚拟 DOM 树的节点,如果新旧节点的标签(tag)、属性(props)、事件等都相同,Vue就不修改直接跳过。 如果节点类型不同(比如,标签类型、组件不同),则整个节点直接替换。

(2)通过节点的 key 属性优化节点比较

Vue通过每个节点的 key 属性来优化比较过程,尤其是当列表(如 v-for)渲染时,key能显著提高性能。

同一层级的节点有 key,根据key的值来追踪节点,实现高效的位置重排和节点复用。

没有 key,依赖节点的位置来进行比较,会导致性能问题(尤其是大型列表)

(3) 最小化 DOM 操作

比较差异之后,Vue 会计算出需要执行的最小 DOM ,然后批量更新来操作实际 DOM。利用文档碎片(DocumentFragment)来批量插入和删除节点,减少对DOM的频繁操作。

(4) 递归更新子树

对于差异节点,Vue递归更新子树。递归操作一直深入到树的叶子节点,直到所有节点都被比较和更新完成。

详细步骤

生成 => 比对 => 检查 => 更新

生成虚拟DOM:视图更新时,生成新的虚拟DOM树 开始比对:将新旧虚拟DOM树进行比对,从根节点开始对比,如果不同,则会递归地对比每一层的子节点。 检查变化:新旧节点相同,则不做更新;新旧节点不同,判断节点类型,如果类型变化,直接替换节点。如果是同类型的节点,则比较属性、事件等差异。 更新 DOM:根据差异生成最小的 DOM 更新(包括增、删、改节点等),然后批量更新到真实 DOM。

优化点

那么vue在diff算法之中有哪些地方比较有优势呢:

key 的使用

列表中的节点每个列表项提供一个唯一的 key 值,Vue 会通过 key 来判断节点的变化位置,以避免全量重渲染(侧面证明了v-for过程之中key在渲染优化方面的不可或缺性)每个列表项提供一个唯一的 key 值。通过 key,框架可以追踪节点的位置,从而优化节点的重排和复用,避免因顺序变化而导致整个列表重渲染。

双端对比

移动元素,Vue 采用双端对比策略,从两边同时比较,快速找到节点的位置和变化。

静态节点提升

对于静态节点(不变化的节点),Vue 会缓存起来,避免不必要的比较和更新

案例

举个例子简单说明一下:

// 旧的虚拟 DOM
const oldVNode = {
  tag: 'div',
  children: [
    { tag: 'p', children: ['Hello'] },
    { tag: 'span', children: ['World'] }
  ]
};

// 新的虚拟 DOM
const newVNode = {
  tag: 'div',
  children: [
    { tag: 'p', children: ['Hello Vue'] },  // 内容发生了变化
    { tag: 'span', children: ['World'] },   // 没有变化
    { tag: 'span', children: ['!'] }        // 新增了一个 span
  ]
};

当vue对比上面的新旧diff结构的时候,分别顺序以下:

比较p标签的内容,发现变化了('Hello'=>'Hello Vue' ),更新该节点的内容。
比较 spa 标签,发现没有变化,所以保持原样。(span没有改变)
发现树中新增了有一个新的 span 节点,所以会在原来的 span 后面插入一个新的 span 标签。(插入了一个span元素))