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;
相关推荐
静听松涛1333 小时前
提示词注入攻击的防御机制
前端·javascript·easyui
晚风予星3 小时前
简记 | 一个基于 AntD 的高效 useDrawer Hooks
前端·react.js·设计
栗子叶3 小时前
网页接收服务端消息的几种方式
前端·websocket·http·通信
菩提小狗3 小时前
Sqli-Labs Less-3 靶场完整解题流程解析-豆包生成
前端·css·less
澄江静如练_3 小时前
优惠券提示文案表单项(原生div写的)
前端·javascript·vue.js
C_心欲无痕3 小时前
ts - 关于Object、object 和 {} 的解析与区别
开发语言·前端·javascript·typescript
L Jiawen3 小时前
【Windows 系统】Chrome浏览器退出登录状态失效
前端·chrome·windows
IT_陈寒4 小时前
Java并发编程实战:从入门到精通的5个关键技巧,让我薪资涨了40%
前端·人工智能·后端
全栈前端老曹4 小时前
【包管理】read-pkg-up 快速上手教程 - 读取最近的 package.json 文件
前端·javascript·npm·node.js·json·nrm·package.json
程序员爱钓鱼4 小时前
Node.js 编程实战:测试与调试 —— 调试技巧与性能分析
前端·后端·node.js