React或Vue的DOM diff算法是怎样的?

155 阅读3分钟

React的diff算法

是什么

DOM diff 就是对比两棵虚拟 DOM 树的算法。当组件变化时,会render 出一个新的虚拟 DOM,diff 算法对比新旧虚拟 DOM 之后,得到一个 patch,然后 React 用 patch 来更新真实 DOM。

怎么做

首先,对比两棵树的根节点

  1. 如果根节点的类型改变了,比如 div 变成了 p,那么直接认为整棵树都变了,不再对比子节点。此时直接删除对应的真实 DOM 树,创建新的真实 DOM 树。
  2. 如果根节点的类型没变,就看看属性变了没有
    a. 如果没变,就保留对应的真实节点
    b. 如果变了,就只更新该节点的属性,不重新创建节点。
    i. 更新 style 时,如果多个 css 属性只有一个改变了,那么 React 只更新改变的。
    然后,同时遍历两棵树的子节点,每个节点的对比过程同上,不过存在如下两种情
    况。

a1. 情况一

<ul>  
    <li>A</li>  
    <li>B</li>  
</ul>  

<ul>  
    <li>A</li>  
    <li>B</li>  
    <li>C</li>  
</ul>

React 依次对比 A-A、B-B、空-C,发现 C 是新增的,最终会创建真实 C节点插入页面。 3. 情况二

<ul>  
    <li>B</li>  
    <li>C</li>  
</ul>  
<ul>  
    <li>A</li>  
    <li>B</li>  
    <li>C</li>  
</ul>

React 对比 B-A,会删除 B 文本新建 A 文本;对比 C-B,会删除 C 文本,新建 B文本;(注意,并不是边对比边删除新建,而是把操作汇总到 patch 里再进行DOM 操作。)对比空-C,会新建 C 文本。

你会发现其实只需要创建 A 文本,保留 B 和 C 即可,为什么 React 做不到呢?
因为 React 需要你加 key 才能做到:

<ul>  
    <li key="b">B</li>  
    <li key="c">C</li>  
</ul>  
<ul>  
    <li key="a">A</li>  
    <li key="b">B</li>  
    <li key="c">C</li>  
</ul>

React 先对比 key 发现 key 只新增了一个,于是保留 b 和 c,新建 a。

Vue的diff算法(双端交叉对比算法):

参考:

  1. canyuegongzi.github.io/web/vue/3.h…
  2. juejin.cn/post/697162…

后续总结

React Dom diff和Vue Dom diff的区别:

  1. React 是从左向右遍历对比,Vue 是双端交叉对比。
  2. React 需要维护三个变量(有点扯),Vue 则需要维护四个变量。
  3. Vue 整体效率比 React 更高,举例说明:假设有 N 个子节点,我们只是把最后子节点移到第一个,那么 a. React 需要进行借助 Map 进行 key 搜索找到匹配项,然后复用节点
    b. Vue 会发现移动,直接复用节点

React DOM diff 代码查看流程:

  1. 运行 git clone github.com/facebook/re…
  2. 运行 cd react; git switch 17.0.2
  3. 用 VSCode 或 WebStorm 打开 react 目录
  4. 打开 packages/react-reconciler/src/ReactChildFiber.old.js 第 1274 行查看旧
    版代码,或打开 packages/react-reconciler/src/ReactChildFiber.new.js 第
    1267 行查看新代码(实际上一样)
  5. 忽略所有警告和报错,因为 React JS 代码中有不是 JS 的代码
  6. 折叠所有代码

  1. 根据 React 文档中给出的场景反复在大脑中运行代码
    a. 场景0:单个节点,会运行到 reconcileSingleElement。接下来看多个节点的
    情况。
    b. 场景1:没 key,标签名变了,最终会走到 createFiberFromElement(存疑)
    c. 场景2:没 key,标签名没变,但是属性变了,最终走到 updateElement 里的
    useFiber
    d. 场景3:有 key,key 的顺序没变,最终走到 updateElement
    e. 场景4:有 key,key 的顺序变了,updateSlot 返回 null,最终走到
    mapRemainingChildren、updateFromMap 和
    updateElement(matchedFiber),整个过程较长,效率较低
  2. 代码查看要点:
    a. 声明不看(用到再看)
    b. if 先不看(但 if else 要看)
    c. 函数调用必看
  3. 必备快捷键:折叠所有、展开、向前、向后、查看定义