图解 React 的 diff 算法:核心就两个字 —— 复用

图解 React 的 diff 算法:核心就两个字 —— 复用
最新回答
沿途有你

2026-07-17 15:57:58

React 是一个基于虚拟 DOM 的前端框架,组件的渲染会生成虚拟 DOM,然后渲染器将其渲染为实际的 DOM。当组件的状态更新时,组件会重新渲染,产生新的虚拟 DOM。在浏览器环境下,为了减少 DOM 的创建,React 会对两次渲染结果进行 diff,尽量复用 DOM,以提高性能。

diff 算法是前端框架中较为复杂的一部分,代码量相对较大,但我们今天不讨论具体的代码实现,而是通过图片来理解这一算法的核心思想。

首先,我们先了解一下 React 的 fiber 架构。React 通过 JSX 来描述页面结构,经过编译后生成的 render 函数会输出虚拟 DOM,即 React Element 的实例。在 React 16 之前,直接递归渲染虚拟 DOM,setState 触发重新渲染,对比新旧虚拟 DOM,对差异部分进行 DOM 操作。然而,在 React 16 之后,为了优化性能,将虚拟 DOM 转换成 fiber,即从树结构转换为链表,然后再进行渲染。整体渲染流程分为两阶段:从虚拟 DOM 转换成 fiber 的过程称为调和(reconcile),这个过程可以被中断,由调度器调度执行。

diff 算法作用在调和阶段:第一次渲染不需要 diff,直接将虚拟 DOM 转换成 fiber。再次渲染时,会产生新的虚拟 DOM,此时需要与之前的 fiber 进行对比,决定如何生成新的 fiber,对可复用的节点标记修改,对旧节点标记删除,对新节点标记新增。

接下来,我们来详细了解一下 React 的 diff 算法。在讲算法实现之前,我们需要思考为什么要做 diff,不做可以吗?当然可以,每次渲染都直接将虚拟 DOM 转换成 fiber 就可以,无需与之前的进行对比,这样是可行的。然而,这并不适用于服务端渲染(SSR),因为 SSR 会将组件渲染成字符串,第二次渲染也是生成字符串,所以这时候不需要与之前的字符串进行对比以找出可以复用的部分。但是浏览器环境中需要这样做,因为 DOM 的创建成本很高,不做 DOM 的复用会导致前端框架的性能下降。diff 算法的目的是对比两次渲染结果,找出可复用的部分,对旧节点标记删除,对新节点标记新增。

那么,React 的 diff 算法是如何实现的呢?比如在一个父节点下有 A、B、C、D 四个子节点,生成的虚拟 DOM 就是这样的。经过调和阶段后,会变成这样的 fiber 结构。如果再次渲染出 A、C、B、E 的虚拟 DOM,这时候如何处理呢?在再次渲染虚拟 DOM 的时候,也需要进行虚拟 DOM 转换成 fiber 的调和阶段,但要尽量复用之前的节点。

如何实现复用呢?通过对比新旧虚拟 DOM 即可。首先将之前的 fiber 节点放到一个 map 中,key 就是节点的 key。然后,每个新的虚拟 DOM 都会在这个 map 中查找是否有可复用的节点,找到后将其移动过来,标记为更新的 effectTag。这样遍历完虚拟 DOM 节点后,map 中剩下一些无法复用的节点,将其删除,标记为删除的 effectTag;如果虚拟 DOM 中还有未找到复用节点的,就直接创建,标记为新增的 effectTag。这样就实现了更新时的调和过程,核心就是找到可复用的节点,对旧节点标记删除,对新节点标记新增。

有时可以简化这个过程,比如上次渲染的顺序与这次渲染的顺序相同,那么直接对比即可,不需要建立 map 再查找。因此,React 的 diff 算法分为两个遍历阶段:第一轮遍历,一一对比新的虚拟 DOM 和旧的 fiber,如果可以复用就进行下一个节点,否则结束遍历。如果所有的新的虚拟 DOM 处理完毕,那么删除剩下的旧 fiber。如果还有虚拟 DOM 未处理,进行第二轮遍历:第二轮遍历,将剩下的旧 fiber 放入 map 中,遍历剩余的虚拟 DOM,从 map 中查找,找到后移动过来标记为更新的 effectTag。第二轮遍历结束后,删除剩余的旧 fiber,新增剩余的虚拟 DOM。这样就完成了新的 fiber 结构的创建,即调和过程。

以上就是 React 的基于 fiber 的渲染流程,分为 render(调和 + 调度)和 commit 两个阶段。当渲染完成后,产生了 fiber,再次渲染的虚拟 DOM 需要与之前的 fiber 进行对比,决定如何生成新的 fiber,目标是尽可能复用现有的 fiber 节点,这就是 diff 算法。

React 的 diff 算法分为两个阶段:第一阶段一一对比,如果可以复用就进行下一个节点,不可以复用就结束。第二阶段将剩下的旧 fiber 放入 map 中,遍历剩余的虚拟 DOM,从 map 中查找,找到后移动过来标记为更新的 effectTag。最后删除剩余的旧 fiber,新增剩余的虚拟 DOM。这样就完成了更新时的调和过程。其实,diff 算法的核心就是复用节点,无论是通过一一对比还是通过 map 查找,都是为了找到可复用的节点,移动过来。然后,对旧节点标记删除,对新节点标记新增。

理解如何找到可复用的节点,就能理解 diff 算法的核心。