React的Diff算法

Diff算法的对比对象

组件重新渲染生成的Element VS 组件对应的oldFiber

两者是用不同的数据结构存储的。

Diff主要是针对多个子节点的情况,单个子节点直接复用oldFiber即可,不需要对比了。

Diff的步骤

diff算法的关键函数是reconcileChildrenArray

如下图,可以看到,

参数是父级Fiber,第一个子节点Fiber,Element新生成的Children数组,更新优先级lane及debugger信息,

返回值是Fiber

第一步,遍历newChildren,复用oldFiber创建新Fiber

javascript 复制代码
  function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<any>,
    lanes: Lanes,
    debugInfo: ReactDebugInfo | null,
  ): Fiber | null {


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

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    // 第一步,遍历newChildren,复用oldFiber
    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,
        debugInfo,
      );
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }

这里会通过sibling指针遍历oldFiber,找到与children对应的oldFiber。oldFiber树是从currentFirstChild开始比较,也就是说大家都是从第一个节点开始对比。

调用updateSlot,其内部会判断tag和key判断是否匹配,如果匹配,复用老的Fiber,形成新的Fiber,如果不匹配,返回null,newFiber为null。

第二步,假如newChildren全部遍历完,统一删除剩余的oldFiber

javascript 复制代码
  // 第二步,假如newChildren全部遍历完,则删除其余的Fiber
    if (newIdx === newChildren.length) {
      // We've reached the end of the new children. We can delete the rest.
      deleteRemainingChildren(returnFiber, oldFiber);
      if (getIsHydrating()) {
        const numberOfForks = newIdx;
        pushTreeFork(returnFiber, numberOfForks);
      }
      return resultingFirstChild;
    }

第三步,oldFiber是null,再次遍历newChildren,创建新Fiber

ini 复制代码
    // 第三步,oldFiber是null,说明newChildren没在第一步顺利完成遍历,再次循环newChildren,创建新Fiber
    if (oldFiber === null) {
      // If we don't have any more existing children we can choose a fast path
      // since the rest will all be insertions.
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(
          returnFiber,
          newChildren[newIdx],
          lanes,
          debugInfo,
        );
        if (newFiber === null) {
          continue;
        }
        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;
      }
      if (getIsHydrating()) {
        const numberOfForks = newIdx;
        pushTreeFork(returnFiber, numberOfForks);
      }
      return resultingFirstChild;
    }

第四步,针对发生移动和更复杂情况

javascript 复制代码
  // 第四步,newChildren没遍历完且oldFiber不为null,针对移动等更复杂情况
    // Add all children to a key map for quick lookups.
    const existingChildren = mapRemainingChildren(oldFiber);

    // Keep scanning and use the map to restore deleted items as moves.
    for (; newIdx < newChildren.length; newIdx++) {//这里的newIndex是上回遍历的结果
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
        debugInfo,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            // The new fiber is a work in progress, but if there exists a
            // current, that means that we reused the fiber. We need to delete
            // it from the child list so that we don't add it to the deletion
            // list.
            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

mapRemainingChildren方法返回一个map,名为existingChildren,map的结构是[老fiber对应的key(或index),老fiber]。

接下来遍历没有处理的children,通过updateFromMap,判断mapRemainingChildren中有没有可以复用的oldFiber,如果有,那么复用,如果没有,新创建一个newFiber。

复用的oldFiber会从existingChildren中被删除。

第五步,统一删除没有复用的oldFiber

javascript 复制代码
// 第五步,删除没有复用的oldFiber
    if (shouldTrackSideEffects) {
      // Any existing children that weren't consumed above were deleted. We need
      // to add them to the deletion list.
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }

    if (getIsHydrating()) {
      const numberOfForks = newIdx;
      pushTreeFork(returnFiber, numberOfForks);
    }
    return resultingFirstChild;
相关推荐
泡海椒3 小时前
JQuick-Excel FORMAT 导出格式实战:日期、金额显示与 TRANSFORM 的边界
前端·python·excel
迅猛龙办公室8 小时前
Python输出当前计算机的系统日期和时间
开发语言·前端·python
小狼154548 小时前
浏览器扩展脚本为什么有时候不生效:注入时机、iframe 和单页路由,多多开票助手
前端·chrome
じòぴé南冸じょうげん9 小时前
油猴脚本突然发现变成灰色了,无法使用?页面不生效?刷新页面没反应?
前端
Csvn10 小时前
组合式 API(Composition API)
前端
火柴就是我11 小时前
Android 打包报错 25.0.3
android·前端
BD_Marathon11 小时前
消息对象中字段的说明
java·前端·python
八荒启·交互动画12 小时前
Web特效025—用 Canvas 2D 做Web特效的定义与边界:这支画笔能做到哪一步
前端·webgl·网页特效·八荒启-交互动画·八荒启
Ai-_Man12 小时前
您您这可以把Dola的多个会话比如说。左侧的多个会话一次性导出吗?不是单条会话里面的多次会对话。用AI导出鸭,答案是可以的
开发语言·前端·人工智能·小程序
excel12 小时前
Nuxt 中使用 useHead 优化 SEO 与 GEO
前端