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;
相关推荐
abigale032 小时前
webpack+vite前端构建工具 -11实战中的配置技巧
前端·webpack·node.js
专注API从业者2 小时前
构建淘宝评论监控系统:API 接口开发与实时数据采集教程
大数据·前端·数据库·oracle
Joker`s smile2 小时前
Chrome安装老版本、不同版本,自制便携版本用于前端调试
前端·chrome
weixin_416639972 小时前
爬虫工程师Chrome开发者工具简单介绍
前端·chrome·爬虫
我是如子啊2 小时前
【解决“此扩展可能损坏”】Edge浏览器(chrome系列通杀))扩展损坏?一招保留数据快速修复
前端·chrome·edge
灵性花火2 小时前
Qt的前端和后端过于耦合(0/7)
开发语言·前端·qt
孤水寒月6 小时前
基于HTML的悬窗可拖动记事本
前端·css·html
祝余呀6 小时前
html初学者第一天
前端·html
耶啵奶膘9 小时前
uniapp+firstUI——上传视频组件fui-upload-video
前端·javascript·uni-app
视频砖家9 小时前
移动端Html5播放器按钮变小的问题解决方法
前端·javascript·viewport功能