加加 React 30 问 -- 2. Diff 算法是什么(1)?

375 阅读5分钟

What -- 什么是 Diff 算法(1)

对于需要 update 的组件,React 会将当前组件与该组件上一次更新后渲染的 Fiber 做比较,这个比较用到的方法,就是 Diff 算法。 Diff 算法就是更新过程中,比对待更新组件与现有组件差别,处理差别的过程 Diff 算法后,会跟生成一些处理 effectTag,然后 React 会根据这些 tag 更新出新的组件

和 Diff 算法相关的概念词语

对于视图上一个已有的 DOM 节点,在 Diff 算法中会有以下概念与其相关

  • current Fiber: 当前 DOM 节点对于的 fiber 节点,是 DOM 节点的 fiber 描述
  • workInProgress Fiber: 如果 DOM 节点将要在本次更新中渲染到页面中,那么 workInProgress Fiber 就是将要替换的 DOM 节点对应的 fiber 节点。也就是更新后的 DOM 对应的 fiber ,但是现在还处于 fiber 节点
  • DOM 节点: 处于视图中,描述某一次页面的节点
  • JSX 对象: ClassComponent 的 render 方法返回的结果,或者 FunctionComponent 的调用结果;JSX 对象和 DOM 节点是关联关系,是描述 DOM 节点响应信息的对象。

Diff 算法就是比对 JSX 对象和 current Fiber,最后生成 workInprogress Fiber 的过程,这个过程也是 reconcile 过程 workInprogress Fiber 最后会被渲染成新的 DOM,这个就是直接渲染。

Why -- 为什么要用 Diff 算法 / 为什么需要特定的 Diff 算法,其他的方法有啥缺点吗?

Diff 算法就是比对 currentFiber 树与 jsx 对象,最后生成 workInprogress 树的过程,其中 jsx 对象也相当于是 dom 树的映射

  • 完全比对两个树的算法,时间复杂度要达到 O(n3)O(n^3), 这个复杂度太高,需要耗费的资源太大,所以归纳常用特性后, React 决定对比对做限制条件,进而降低复杂度,而这些限制条件下的比对算法,就是 Diff 算法。

How1 -- Diff 算法是怎样的 / 有限制的算法中的限制是什么

总原则

  1. 只对同级元素进行 Diff。如果一个 DOM 节点在更新前后跨越了层级,那么 React 会在 WIP Fiber 树对应的位置删除旧节点树,然后再新的位置重新构建
  2. 两个不同类型的元素会产生不同的树。DOM 节点更改了它的标签,对应于 fiber 就是类型发现了变化,例如 div -> p,那么会先删除 div 对应的整个子树,然后再在对应的位置新增一个 p 树
  3. 使用 key prop 来暗示那些元素在不同的渲染下能保持稳定

理解

Q: 首先我们要定位,什么时候才叫 DOM 节点跨越了层级,怎么判断更新前后两个元素是同一个元素呢?

  • 如果有 key,且 key 唯一,那么就可以按照 key 来判断更新前后的节点是哪一组两个
  • 如果没有 key,那么就按照位置算,比方说 div > p a p 变成了 div > a p p,如果没有 key 标志,就会先删除 p,然后再对应生成一个 a;然后再删除 a,对应位置生成 p

Q: key 的作用

  • 大力出奇迹,没有 key 的时候,直接同一级对比,一旦类型不对,直接删除全部,重建就好
  • 如果有 key 了,那么每一个 jsx 对应的节点就需要遍历 current Fiber 同一层级的所有的节点,找出 key 相同的那个节点,然后再看看是删除还是更新;

Q: 为什么说 key 不一定提升效率

  • 虽然 key 可以避免位置转移的更新,不效率的使用删除新增的方法进行更新
  • 但是存在 key 的时候,每一个节点都需要遍历所有的同层级节点,找到对应的节点,这个遍历过程本来就是一种资源消耗,如果在叶子节点中,那么删除新增的方法,会比遍历查找后更新,耗费更少的资源。
  • 当然 React 是推荐使用 key 的,因为统计来看,一次性删除新增那么大的子树,比遍历查找耗费的资源多的多。

How2 -- Diff 是如何实现的

概述

  • Diff 算法通过 DFS 的方式 JSX 节点的 child 去和同一层级的 fiber 进行对比,然后返回对应的结果
  • 首先判断 child 的类型来处理,child 可能是对象、字符串、数组等等
  • Diff 算法有两种: 单节点 Diff 和多节点 Diff
  • 单节点就是 child 只有一个,这个时候 child 可能是 string/number,统一当文档处理;如果是对象,则就是一个组件,那么就再次分类了
    • REACT_ELEMENT_TYPE 普通组件的处理
    • REACT_PORTAL_TYPE 入口组件的出来
    • REACT_LAZY_TYPE xxx
  • 多节点就是 child 是数组或者类数组,那么就需要更复杂的比对方法了
function reconcileChildFibers(
    returnFiber, // 父 fiber
    currentFirsetChild, // 当前层级的第一个 child,
    newChild, // jsx 的一个节点对应的对象
) {
    // 如果
    const isObject = typeof newChild === 'object' && newChild !== null;

    if (isObject) {
        // object 类型,可能是 REACT_ELEMENT_TYPE 或者是 REACT_PORTAL_TYPE 或者 REACT_LAZY_TYPE
        switch (newChild.$$typeof) {
            case REACT_ELEMENT_TYPE:
            //todo
            case REACT_PORTAL_TYPE:
            //todo
            case REACT_LAZY_TYPE:
            //todo
        }
    }

    // 使用单节点文本的diff 算法
    if (typeof newChild === 'string' || typeof newChild === 'number') {
        // 调用 reconcileSingleTextNode 处理文本节点
        return placeSingleChild(
            reconcileSingleTextNode(
                returnFiber,
                currentFirstChild,
                '' + newChild,
                lanes,
            ),
        );
    }

    // 数组
    if (isArray(newChild)) {
        // 数组
        return reconcileChildrenArray(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
        );
    }

    //   类数组
    if (getIteratorFn(newChild)) {
        // Interator,和数组类似
        return reconcileChildrenIterator(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
        );
    }

    // 其他情况,直接删除节点直接新增
    return deleteRemainingChildren(returnFiber, currentFirstChild);
}

各个 Diff 算法的实现

未完待续...

小结

  1. Diff 算法发送在 React 的 reconcile 阶段,通过比对 jsx 对象和当前展示在视同中的 dom 对应的 currentFiber 做比对和处理的方法。
  2. 因为完全比对两课树的复杂度太高,耗费资源太多,所以根据不为人知的统计后,设计了一种限制性的比对处理方法,就是 Diff 方法了
  3. key 是 Diff 方法中很重要的一个属性,关乎在多节点 Diff 中使用哪种方式处理节点,但是使用 key 并不一定就是优化性能了。
  4. Diff 有三大规范:同层级比对,不同类型节点删除,key的使用
  5. 比对过程中,给定 jsx child 是对象数组形式,而同层级 fiber 是一种链式结构,通过 sibling 链接对应的兄弟 fiber。

参考

React 技术揭秘 -- Diff 算法