likes
comments
collection
share

别慌, react diff 没有那么复杂前言 从前对各种底层原理都比较抵触,总觉得极其复杂,肯定不是自己这只菜鸟能搞定

作者站长头像
站长
· 阅读数 19

前言

从前对各种底层原理都比较抵触,总觉得极其复杂,肯定不是自己这只菜鸟能搞定的。但随着技术的成长,再回头看这些,也逐渐明白了那些令我恐惧的东西是什么。

本文会以尽可能简单清晰的话来说清楚 react diff,希望能帮助到你。

为什么要做 diff

举个例子,在没有框架的情况下,当你需要这样的 html 变更时:

    // dom结构 一
    <div>
        <p>我是p元素</p>
        <div id="乱七八糟" class="xxxxx" (这里还有一堆属性)>后面需要被改变的标签<div>
    </div>
    
    // dom结构 二
    <div>
        <p>我是p元素</p>
        <div>节点被更改<div>
    </div>
    

正常的想法应该是删除 div 标签,再搞个新的 div 插进去

const oldDiv = document.getElementById('乱七八糟');
oldDiv.parentNode.removeChild(oldDiv); // 删除旧的 div
const newDiv = document.createElement('div');
newDiv.innerHTML = '节点被更改'; // 创建新的 div
oldDiv.parentNode.appendChild(newDiv); // 添加新的 div

但创建节点其实是很消耗性能的, 更好的做法应该是,删除/新增/修改掉前面的 div 的属性之类的,不过这样写起来会相当繁琐,我们通常会抽出来类似这样的方法:

function updateElement(oldElement, newElement) {
    // 更新旧元素的内容
    oldElement.innerHTML = newElement.innerHTML;
    
    // 更新旧元素的属性
    Array.from(newElement.attributes).forEach(attr => {
        oldElement.setAttribute(attr.name, attr.value);
    });
    const oldAttributes = Array.from(oldElement.attributes);
    oldAttributes.forEach(attr => {
        if (!newElement.hasAttribute(attr.name)) {
            oldElement.removeAttribute(attr.name);
        }
    });
}

这么一来,我们就可以比较方便的实现节点的更新了。但这只是说明了节点如何变更,但变更的前提应该是:节点如何比较?

这就引出了 diff 算法了,它的好处在于:

  1. 自动化:你只需要提供两棵虚拟 DOM 树,Diff 算法会自动比较它们,找出差异。

  2. 性能优化:React 的 diff 算法通过一些优化手段(如分层比较、利用 key 值等),提高了比较和更新的性能

diff 的过程是怎么样的

现在你知道了,diff 大概就是这样一个函数:传入两棵树,传出变更内容

function diff(nodeTree1, nodeTree2) {
    return diffContent;
}

我们再来看一下 diff 具体的过程吧,首先对比会分为三种:

  1. tree diff,节点树对比

    1. 逐层逐节点的对比,判断是否有节点出现差异
    2. 如果该节点是组件节点,进入 component diff
    3. 如果该节点是元素节点,进入 element diff
  2. component diff,组件对比

    1. 组件名不同时,打删除,新增 tag
    2. 相同时,打更新 tag
    3. 递归组件子节点,进行 tree diff
  3. element diff,元素对比

    1. 标签名不同时,打删除,新增 tag
    2. 相同时,打更新 tag
    3. 递归组件子节点,进行 tree diff

这就是简化版的 diff 算法核心了,看到这一步,你已经学会七七八八了, 让我们更进一步吧。

了解完这个核心,相信你也会提出问题:刚才似乎都是说一个子节点的情况,多个子节点要怎么和多个子节点进行对比呢?

  • a { b {c} } 对比 a { b {d} } ,按上述原则很好解决,每层只有一个节点,不需要什么判断
  • a { b c } 对比 a { c b } ,涉及到了多节点的对比,这样怎么处理,难道 bc 和 cb 仅仅因为顺序不同就 diff 了吗,这不浪费吗

是的,这里其实没有太多技巧,diff 用的是最简单的按顺序逐一对比的方案

按照逐个对比来看:

  1. 新树中第一个为 b,而旧树种第一个为 c,所以这个 b 要删除,并且新增一个 c
  2. 新树中第二个为 c,而旧树种第一个为 b,所以这个 c 要删除,并且新增一个 b
  3. 结束

相信你也能看出来,这很浪费吧?所以 react 这里做了一些优化

react diff 的 key 优化

通过增加 key,react 解决了上述的问题,diff 会分为两次遍历

第一轮遍历

  1. 线性对比:React 会从头开始线性地比较新旧节点。

    1. 如果新旧节点类型key 都相同,则标记为更新节点。
    2. 如果新旧节点类型key 存在不同,则标记为删除旧节点并创建新节点。
  2. 结束条件:当发现新旧节点类型不同时,线性遍历会停止,进入第二轮遍历。

第二轮遍历

  1. 创建 Map:将旧树中剩余的节点放入一个 Map 中,key 为节点的 key 值,值为对应的节点。

  2. 查找复用节点:继续遍历新树中的剩余节点,尝试在 Map 中查找可复用的节点。

    1. 如果在 Map 中找到相同 key 的节点,则标记为更新节点,并从 Map 中删除该节点(因为已经复用了)。
    2. 如果找不到相同 key 的节点,则标记为创建新节点。
  3. 处理剩余节点:遍历完成后,Map 中剩余的节点都是需要删除的旧节点。

具体例子

假设旧树是:

<ul>
  <li key="A">A</li>
  <li key="B">B</li>
  <li key="C">C</li>
  <li key="D">D</li>
</ul>

新树是:

<ul>
  <li key="A">A</li>
  <li key="C">C</li>
  <li key="E">E</li>
</ul>

第一轮遍历

  • 比较第一个节点 A,发现相同,标记为更新。
  • 比较第二个节点 BC,发现不同,标记为删除旧节点 B 并创建新节点 C
  • 继续比较,发现第三个节点 CE 类型不同,标记为删除旧节点 C 并创建新节点 E
  • 继续比较,发现旧节点 D 没有对应的新节点,标记为删除。

第二轮遍历

  • 将旧树中剩余的节点 BCD 放入 Map 中:

     Map = { 
         B: <Fiber for B>, 
         C: <Fiber for C>, 
         D: <Fiber for D> 
     }
     ```
    
    
  • 继续遍历新树中的剩余节点:

    • 找到 C,在 Map 中找到相同 key 的节点,标记为更新,并从 Map 中删除:

       Map = {
           B: <Fiber for B>, 
           D: <Fiber for D> 
       }
       ```
      
      
    • 找到 E,在 Map 中找不到相同 key 的节点,标记为创建新节点。

  • 最后,Map 中剩余的节点 BD 都标记为删除。

最后

恭喜,看到这里,你已经理解了一个 react diff 的核心过程了。如果这篇文章对你有帮助,就点个赞吧。

转载自:https://juejin.cn/post/7424009003922391050
评论
请登录