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;
相关推荐
ZhaoJuFei5 分钟前
React生态学习路线
前端·学习·react.js
早點睡3908 分钟前
ReactNative项目OpenHarmony三方库集成实战:react-native-calendar-events(读取不到日历里新增的事件,待排查)
javascript·react native·react.js
apcipot_rain22 分钟前
CSS知识概述
前端·css
837927397@QQ.COM24 分钟前
个人理解无界原理
开发语言·前端·javascript
冰暮流星27 分钟前
javascript之Dom查询操作1
java·前端·javascript
admin and root29 分钟前
XSS之Flash弹窗钓鱼
前端·网络·安全·web安全·渗透测试·xss·src
Cxiaomu1 小时前
像ChatGPT一样逐字输出:React + TypeScript 流式接收与“打字机”效果实现方案
人工智能·react.js·chatgpt·typescript
qq_381338501 小时前
[特殊字符] MonkeyCode AI 核心功能详解
前端
万邦科技Lafite1 小时前
淘宝关键词API接口获取分类商品信息指南
java·前端·数据库·开放api·淘宝开放平台
KevinCyao1 小时前
教育营销短信接口开发实战:培训机构接入营销短信API实现招生信息精准触达
前端