React解读--diff算法

869 阅读5分钟

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

diff算法的On3怎么来的

在react架构中,我提到上一次渲染的Fiber也就是已经呈现在页面中的结点是current fiber,本次更新是workinprogress fiber。我们都知道react的diff算法是从O(n3)的时间复杂度变成了O(n),这里说一下O(n3)是怎么来的:

  1. 将两颗树中所有的节点一一对比需要O(n²)的复杂度,
  2. 在对比过程中发现旧节点在新的树中未找到,那么就需要把旧节点删除,删除一棵树的一个节点(找到一个合适的节点放到被删除的位置)的时间复杂度为O(n),同理添加新节点的复杂度也是O(n),合起来diff两个树的复杂度就是O(n³)

diff算法的优化

为了降低算法复杂度,Reactdiff会预设三个限制:

  1. 只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。
  2. 两个不同类型的元素会产生出不同的树。如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。
  3. 开发者可以通过 key prop来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:

diff算法

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

image-20220113163136019

// 根据newChild类型选择不同diff函数处理
function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
): Fiber | null {
  const isObject = typeof newChild === 'object' && newChild !== null;
  if (isObject) {
    // object类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        // 调用 reconcileSingleElement 处理
      // // ...省略其他case
    }
  }

  if (typeof newChild === 'string' || typeof newChild === 'number') {
    // 调用 reconcileSingleTextNode 处理
    // ...省略
  }

  if (isArray(newChild)) {
    // 调用 reconcileChildrenArray 处理
    // ...省略
  }

  // 一些其他情况调用处理函数
  // ...省略

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

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

  1. newChild类型为objectnumberstring,代表同级只有一个节点
  2. newChild类型为Array,同级有多个节点。

单节点的diff

单个节点的diff会进入reconcileSingleElement方法,这个方法主要做的事情其实是

image-20220113143143056

我们可以大致的看看源码中是怎么实现的

  function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;

    // 判断存在的dom结点
    while (child !== null) {
      // 判断上一次的dom结点是否可以复用

      // 比较key
      if (child.key === key) {
        switch (child.tag) {
          case Fragment: {
            if (element.type === REACT_FRAGMENT_TYPE) {
              deleteRemainingChildren(returnFiber, child.sibling);
              const existing = useFiber(child, element.props.children);
              existing.return = returnFiber;
              return existing;
            }
            break;
          }
          case Block:
            if (enableBlocksAPI) {
              let type = element.type;
              if (type.$$typeof === REACT_LAZY_TYPE) {
                type = resolveLazyType(type);
              }
              if (type.$$typeof === REACT_BLOCK_TYPE) {
                // The new Block might not be initialized yet. We need to initialize
                // it in case initializing it turns out it would match.
                if (
                  ((type: any): BlockComponent<any, any>)._render ===
                  (child.type: BlockComponent<any, any>)._render
                ) {
                  deleteRemainingChildren(returnFiber, child.sibling);
                  const existing = useFiber(child, element.props);
                  existing.type = type;
                  existing.return = returnFiber;
                  return existing;
                }
              }
            }
          default: {
            if (child.elementType === element.type)
              // key相同同事type也相同,表示可以复用,返回复用的fiber
               {
              deleteRemainingChildren(returnFiber, child.sibling);
              const existing = useFiber(child, element.props);
              existing.ref = coerceRef(returnFiber, child, element);
              existing.return = returnFiber;
              return existing;
            }
            break;
          }
        }
        // key相同type不同,将该fiber和兄弟fiber标记为删除
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        // key不同,标记删除
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }
    //  创建新的fiber
    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;
    }
  }

从代码中,我们可以看出React首先会判断key是否相同,key不相同直接进入删除逻辑,key相同:

  • type相同:可以进行复用
  • type不同:执行deleteRemainingChildrenchild及其兄弟fiber都标记删除。

多结点的diff

如果是多个结点的diff,他的children属性是一个包含多个节点的数组。那么reconcileChildFibersnewChild参数类型为Array,在reconcileChildFibers函数内部对应如下情况

// 多个节点之间的diff
if (isArray(newChild)) {
  return reconcileChildrenArray(
    returnFiber,
    currentFirstChild,
    newChild,
    lanes,
  );
}

多节点的diff情况比较复杂,分为如下几种情况:

  • 节点更新
    • 节点的属性的变化
    • 节点的类型的变化
  • 节点的增删
  • 节点的位置的变化

注意:

在我们做数组相关的算法题时,经常使用双指针从数组头和尾同时遍历以提高效率,但是这里却不行。

虽然本次更新的JSX对象 newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber,同级的Fiber节点是由sibling指针链接形成的单链表,即不支持双指针遍历。

newChildren[0]fiber比较,newChildren[1]fiber.sibling比较。

所以无法使用双指针优化

因为在开发中大部分都是结点的更新操作,所以React会优先处理节点的更新操作。

第一轮遍历

首先让newChildren[i]oldFiber对比,然后让i++、nextOldFiber = oldFiber.sibling。在第一轮遍历中,会处理三种情况,其中第1,2两种情况会结束第一次循环

  1. key不同,第一次循环结束
  2. newChildren或者oldFiber遍历完,第一次循环结束
  3. key同type不同,标记oldFiber为DELETION
  4. key相同type相同则可以复用

newChildren遍历完,oldFiber没遍历完,在第一次遍历完成之后将oldFiber中没遍历完的节点标记为DELETION,即删除的DELETION Tag

第一轮遍历的代码:

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber; // nextOldFiber赋值
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling; // nextOldFiber赋值oldFiber的兄弟节点
      }
      const newFiber = updateSlot( //更新结点如果key不同则newFiber = null
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break; //跳出第一次遍历
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          // 匹配了 slot,但没有重用现有的 Fiber,删除现有的 child。
          deleteChild(returnFiber, oldFiber);
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); //标记插入节点的位置
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

第二轮遍历

  1. newChildren和oldFiber都遍历完:多节点diff过程结束
  2. newChildren没遍历完,oldFiber遍历完,将剩下的newChildren的节点标记为Placement,即插入的Tag
  3. newChildren和oldFiber没遍历完,则进入节点移动的逻辑

第二轮遍历源码:

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;// 返回diff之后的第一个结点
    }

第三轮遍历

第三轮遍历的主要逻辑,在placeChild函数中,例如更新前节点顺序是ABCD,更新后是ACDB,假设type都相同

image-20220113172831174

  1. newChildren中第一个位置的A和oldFiber第一个位置的A,key相同可复用,lastPlacedIndex=0。
  2. newChildren中第二个位置的C和oldFiber第二个位置的B,key不同跳出第一次循环,将oldFiber中的BCD保存在map中
  3. 继续遍历newChildren,newChildren中第二个位置的C在oldFiber中的index=2 > lastPlacedIndex=0不需要移动,lastPlacedIndex=2
  4. newChildren中第三个位置的D在oldFiber中的index=3 > lastPlacedIndex=2不需要移动,lastPlacedIndex=3
  5. newChildren中第四个位置的B在oldFiber中的index=1 < lastPlacedIndex=3,移动到最后

我们再来看一个例子:

image-20220113173533655

  1. 第一次遍历,key不相同,我们把整个oldFiber保存在map中,lastPlacedIndex=0
  2. 继续遍历newChildren,第一个节点D在oldFiber中存在,index = 3 > lastPlacedIndex = 0,不需要移动
  3. 第二个节点A在oldFiber结点中存在,index = 0 < lastPlacedIndex = 3,移动到最后
  4. 第三个节点B在oldFiber结点中存在,index = 1 < lastPlacedIndex = 3,移动到最后
  5. 第四个节点C在oldFiber结点中存在,index = 2 < lastPlacedIndex = 3,移动到最后

这里不得不提一嘴,就是要尽量减少将节点从后面移动到前面的操作。从上例子我们可以看出,我们从ABCD变成DABC,并不是把D移动到最前面,而是将D不动,ABC移动到最后面。

第三次遍历的源码:

for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap( //从map中获取到fiber
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            // 新的fiber是一个workinprogress,但是这儿存在一个current,
            // 我们要复用这个fiber就需要从childlist中删除,而不是添加deletionlist
            existingChildren.delete( //找到删除的结点
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        // 标记为插入逻辑,得到lastPlacedIndex
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); 
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

整个过程源码:

  function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number,
    newIndex: number,
  ): number {
    newFiber.index = newIndex;
    if (!shouldTrackSideEffects) {
      // Noop.
      return lastPlacedIndex;
    }
    const current = newFiber.alternate;
    if (current !== null) {
      const oldIndex = current.index;
      if (oldIndex < lastPlacedIndex) {
        // oldIndex < lastPlacedIndex 将结点插入到后面
        newFiber.flags = Placement;
        return lastPlacedIndex;
      } else {
        //不需要移动
        return oldIndex;
      }
    } else {
      // 这是新增插入
      newFiber.flags = Placement;
      return lastPlacedIndex;
    }
  }
 function reconcileChildrenArray(
    returnFiber: Fiber, // 父fiber结点
    currentFirstChild: Fiber | null, //childs中第一个节点
    newChildren: Array<*>, // 新结点数组
    lanes: Lanes, // 优先级
  ): Fiber | null {
    let resultingFirstChild: Fiber | null = null; //diff之后返回的第一个节点
    let previousNewFiber: Fiber | null = null; // 新节点中上次对比过的结点

    let oldFiber = currentFirstChild; //正在对比的oldFiber
    let lastPlacedIndex = 0; //上次可以复用的结点的位置或者oldFiber的位置
    let newIdx = 0; //新结点中对比到了的位置
    let nextOldFiber = null; //正在对比的oldFiber

    // 开始第一轮遍历
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber; // nextOldFiber赋值
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling; // nextOldFiber赋值oldFiber的兄弟节点
      }
      const newFiber = updateSlot( //更新结点如果key不同则newFiber = null
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          // 匹配了 slot,但没有重用现有的 Fiber,删除现有的 child。
          deleteChild(returnFiber, oldFiber);
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); //标记插入节点的位置
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {
      // We've reached the end of the new children. We can delete the rest.
      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) { // 返回diff之后的第一个结点
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }

    // 将剩下的oldFiber加入map中
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // 第三次循环 处理节点移动
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap( //从map中获取到fiber
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            // 新的fiber是一个workinprogress,但是这儿存在一个current,
            // 我们要复用这个fiber就需要从childlist中删除,而不是添加deletionlist
            existingChildren.delete( //找到删除的结点
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        // 标记为插入逻辑,得到lastPlacedIndex
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); 
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    if (shouldTrackSideEffects) {
      // 循环结束删除existingChildren中剩下的节点
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }

    return resultingFirstChild;
  }