React Diff 算法

853 阅读4分钟

kalden-swart-BHSsxJ_QNFc-unsplash.jpg

图片来源 unsplash.com/photos/BHSs…

前言

我们知道比较两颗 DOM 树即使使用最优的算法,该算法的复杂程度仍为 O(n³) ,其中 n 是树中元素的数量。如果在 React 中使用该算法,那么展示 1000 个元素则需要 10 亿次的比较。这个开销实在是太过高昂,所以 React 必须降低 diff 算法的复杂度。

最优的 DOM Diff 算法

这里涉及到一个概念:Tree edit distance(树的编辑距离),我们先来了解一下编辑距离

  • 编辑距离

编辑距离指通过计算将一个字符转化为例一个字符串所需的最小操作数来量化两个字符串差异程度。

比如计算 kitten 与 sitting 的编辑距离:

  1. kitten → sitten(将 k 改为 s )
  2. sitten → sittin(将 e 改为 i )
  3. sittin → sitting(最后加入 g )

所以编辑距离为 3 ,算法参考 Levenshtein 的算法。

  • 树的编辑距离(Tree edit distance)

树的编辑距离就是指将一颗 Tree 映射到例外一颗 Tree 所需的最小操作数。

未命名绘图 (23).png

最优的 DOM Diff 算法复杂度 O(n³) 由来

传统 Diff 算法需要找到两个树的最小更新方式,所以需要两两对比这两棵树每个叶子节点是否相同,对比就需要 O(n^2) 的时间复杂度,找到两棵树差异后需要更新(移动、创建、删除)再遍历一次,所以是 O(n^3)

React Diff 算法

React 对于 update 的组件,他会将当前组件与该组件在上次更新时对应的 Fiber 节点比较(也就是 Diff 算法),将比较的结果生成新 Fiber 节点。

相关概念 :

  1. current Fiber。如果该DOM节点已在页面中,current Fiber 代表该 DOM 节点对应的 Fiber 节点。

  2. workInProgress Fiber。如果该 DOM 节点将在本次更新中渲染到页面中,workInProgress Fiber 代表该 DOM 节点对应的 Fiber 节点。

  3. JSX 对象。即 ClassComponent 的 render 方法的返回结果,或 FunctionComponent 的调用结果。JSX 对象中包含描述 DOM 节点的信息。

Rect Diff 算法的本质是对比 1 (current Fiber)3 (JSX 对象),生成 2 (workInProgress Fiber)

React 为降低 DOM diff 算法的复杂度,会预设三个限制 :

  1. 只对同级元素进行 Diff 算法。如果一个 DOM 节点在前后两次更新中跨越了层级,那么 React 不会尝试复用他。

  2. 两个不同类型的元素会产生出不同的树。如果元素由 div 变为 p ,React 会销毁 div 及其子孙节点,并新建 p 及其子孙节点。

  3. 开发者可以通过设置 key 属性,来告知渲染哪些子元素在不同的渲染下可以保存不变;

只同级比较,React diff 就可以将时间复杂度降低为 O(n)。

React Diff 算法的入口函数 reconcileChildFibers 出发,该函数会根据 newChild(即 JSX 对象)类型调用不同的处理函数。

reconcileChildFibers 源码

// 根据newChild类型选择不同diff函数处理
function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
   
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
      
    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    }

    
    if (typeof newChild === 'object' && newChild !== null) {
      // object类型
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_PORTAL_TYPE:
          return placeSingleChild(
            reconcileSinglePortal(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_LAZY_TYPE:
          if (enableLazyElements) {
            const payload = newChild._payload;
            const init = newChild._init;
            return reconcileChildFibers(
              returnFiber,
              currentFirstChild,
              init(payload),
              lanes,
            );
          }
      }

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

      if (getIteratorFn(newChild)) {
        return reconcileChildrenIterator(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }

      throwOnInvalidObjectType(returnFiber, newChild);
    }

    if (typeof newChild === 'string' || typeof newChild === 'number') {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild,
          lanes,
        ),
      );
    }

   
    .....

    // 以上都没有命中,删除节点
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

returnFiber 代表了 currentFiber 的父级 Fiber, currentFirstChild 代表 currentFibernewChild 代表 JSX 对象,lanes 表示优先级相关。

我们可以从同级的节点数量将 Diff 分为两类:

  1. newChild 类型为 object、number、string,代表同级只有一个节点;

  2. 当 newChild 类型为 Array,同级有多个节点;

单一节点的 diff

对于单一节点,我们以类型 object 为例,会进入 reconcileSingleElement

if (typeof newChild === 'object' && newChild !== null) {
     switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
         .....
       }
}

reconcileSingleElement 源码

function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    while (child !== null) {
      if (child.key === key) {
        const elementType = element.type;
        if (elementType === REACT_FRAGMENT_TYPE) {
          if (child.tag === Fragment) {
            deleteRemainingChildren(returnFiber, child.sibling);
            const existing = useFiber(child, element.props.children);
            existing.return = returnFiber;
            return existing;
          }
        } else {
          if (
            child.elementType === elementType ||
            // Keep this check inline so it only runs on the false path:
            (__DEV__
              ? isCompatibleFamilyForHotReloading(child, element)
              : false) ||
            (enableLazyElements &&
              typeof elementType === 'object' &&
              elementType !== null &&
              elementType.$$typeof === REACT_LAZY_TYPE &&
              resolveLazy(elementType) === child.type)
          ) {
            deleteRemainingChildren(returnFiber, child.sibling);
            const existing = useFiber(child, element.props);
            existing.ref = coerceRef(returnFiber, child, element);
            existing.return = returnFiber;
            
            return existing;
          }
        }
        // Didn't match.
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }

    if (element.type === REACT_FRAGMENT_TYPE) {
      const created = createFiberFromFragment(
        element.props.children,
        returnFiber.mode,
        lanes,
        element.key,
      );
      created.return = returnFiber;
      return created;
    } else {
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
  }

reconcileSingleElement 方法会做如下事情:

未命名绘图 (24).png

上次更新时的 Fiber 节点不存在对应的 DOM 节点

function App() {
	const [num, setNum] = useState(0);

	return (
		<div className="App" 
                onClick={() => {
		  setNum(num + 1);
		}}>
		    <h1>{num}</h1>
		</div>
	);
}

export default App;

以上面的代码 div 为例,首次渲染时 currentFirstChild 为 null 。此时会调用 createFiberFromElement 函数新生成一个 Fiber 节点并返回。

if (element.type === REACT_FRAGMENT_TYPE) {

     ......
     
    } else {
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }

createFiberFromElement 源码

上次更新时的 Fiber 节点存在对应的 DOM 节点

  • DOM 节点可复用
 function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    while (child !== null) {  // 上次更新时的 Fiber 节点存在对应的 DOM 节点
      if (child.key === key) { // 比较key 是否相同
        const elementType = element.type;
        if (elementType === REACT_FRAGMENT_TYPE) {
          if (child.tag === Fragment) {
            deleteRemainingChildren(returnFiber, child.sibling);
            const existing = useFiber(child, element.props.children);
            existing.return = returnFiber;
            return existing;
          }
        } else {
          if (
            child.elementType === elementType ||
            isCompatibleFamilyForHotReloading(child, element) ||
            (enableLazyElements &&
              typeof elementType === 'object' &&
              elementType !== null &&
              elementType.$$typeof === REACT_LAZY_TYPE &&
              resolveLazy(elementType) === child.type)
          ) {// key 相同,type 相同
          
            // 将该fiber 兄弟fiber标记为删除
            deleteRemainingChildren(returnFiber, child.sibling);
            
            // 复制上次更新时的Fiber节点作为本次更新的Fiber 节点并返回
            const existing = useFiber(child, element.props);
            
            existing.ref = coerceRef(returnFiber, child, element);
            existing.return = returnFiber;
            
            return existing;
          }
        }
        // 代码执行到这里代表:key相同但是type不同
        // 将该fiber及其兄弟fiber标记为删除
        deleteRemainingChildren(returnFiber, child);
        break;
      } else { // key不同,将该fiber标记为删除
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }

    if (element.type === REACT_FRAGMENT_TYPE) {
      const created = createFiberFromFragment(
        element.props.children,
        returnFiber.mode,
        lanes,
        element.key,
      );
      created.return = returnFiber;
      return created;
    } else {
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
  }

多节点的 diff

多节点返回的 JSX 对象的 children 是一个数组,比如下面的例子。

return (
		<div className="App">
			<ul>
				<li key="0">0</li>
				<li key="1">1</li>
				<li key="2">2</li>
			</ul>
		</div>
	);

截屏2021-07-03 上午11.24.07.png

此时 reconcileChildFibersnewChild 参数类型为 Array。


....

if (isArray(newChild)) {
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }
      
.....

reconcileChildrenArray 源码

  function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ): Fiber | null {

    let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          deleteChild(returnFiber, oldFiber);
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
      
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }

    if (oldFiber === null) {
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }

    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    if (shouldTrackSideEffects) {
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }

    return resultingFirstChild;
  }

几个重要变量的含义:

  1. resultingFirstChild : 代表 workInProgress Fiber,通过 sibling 指针连接兄弟节 点。

  2. previousNewFiber : 中间变量,将上一次生成的 Fiber 节点与本次生成的 Fiber 节点连接( previousNewFiber.sibling = newFiber )。

  3. oldFiber : 代表当前遍历到的 current Fiber 节点。

  4. nextOldFiber : 代表当前遍历到的 current Fiber 节点下一个 Fiber 节点。

  5. newIdx : 代表当前遍历到的 JSX 对象在数组中的索引 。

  6. lastPlacedIndex :代表了新创建的 Fiber 节点对于的 DOM 节点在页面中所处的位置。

reconcileChildrenArray 方法最终会返回 resultingFirstChild ;

参考资料

React技术揭秘