React源码之diff算法 上篇 reconcileChildrenArray

208 阅读5分钟

著有《React 源码》《React 用到的一些算法》《javascript地月星》等多个专栏。欢迎关注。

文章不好写,要是有帮助别忘了点赞,收藏~ 你的鼓励是我继续挖干货的的动力🔥。

另外,本文为原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解~

介绍

本篇对照源码和jsx模版简单介绍React Fiber子节点的diff算法。

JSX 模版

情况1:

假如构建这样的树: li-a对应正文1.2顺序比较key和type都相同

<ul>
    <li key="a">1</li>
    <li key="b">2</li>
</ul>

更改元素的类型后:p-b对应1.2.2key相同type不同

<ul>
    <li key="a">1</li>
    <p key="b">2</p>
</ul>
情况2: 调换li元素的位置。第一个li对应正文1.2顺序比较key不同,然后直接跳到正文4。
<ul>
    <li key="a">1</li>
    <li key="b">2</li>
</ul>
<ul>
    <li key="b">2</li>
    <li key="a">1</li>
</ul>
情况3: 对应正文1.1

假如构建这样的树:

const [hid, setHid] = useState(true)
<ul>
    <li key="a">1</li>
    {hid} ? null : <li key="b">2</li> // 结果为null
    <li key="c">3</li>
</ul>

这个模版被创建后得到还没有创建fiber的虚拟dom li

截屏2025-05-12 下午10.43.54.png

相当于jsx: [a, null, c] , 下标index: 0,1,2。

reconcileChildrenArray中循环创建了fiber后: [a, c], 下标index: 0,2。

因为jsx存在了“空洞”,从而fiber.index出现了“跳跃”。

到setHid(false)更新了hid,

<ul>
    <li key="a">1</li>
    {hid} ? null : <li key="b">2</li> // 结果为2
    <li key="c">3</li>
</ul>

reconcileChildrenArray第一阶段的for能够触发if (oldFiber.index > newIdx) {分支。因为旧树index的“跳跃”oldFiber.index = 2时,newIdx = 1。

问:旧fiber怎么会是空槽呢?空.sibling报错的,没办法用sibling获取兄弟节点。并且新树渲染后变成旧树,新树不会创建null的fiber,新树不会有空槽,所以旧树也不会有。假如真的存在空槽,那么oldFiber往后移动了一位,就会出现oldFiber.index>newIdx的情况?

答:正如前面,jsx出现空槽,而不是fiber。上图可以看到ul的子虚拟dom li。第一次是current树是空的,循环jsx创建的fiber,fiber.index是循环jsx的下标,循环到第二次jsx是null不创建fiber,循环到第三次创建fiber,fiber.index是2。

正文 reconcileChildrenArray

function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {
  {
    var knownKeys = null;

    for (var i = 0; i < newChildren.length; i++) {
      var child = newChildren[i];
      knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
    }
  }

  var resultingFirstChild = null;
  var previousNewFiber = null;
  var oldFiber = currentFirstChild;
  var lastPlacedIndex = 0;
  var newIdx = 0;
  var nextOldFiber = null;
  //  1 一对一顺序比较新的子节点和旧的子节点
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { 
     //  1.1 
    if (oldFiber.index > newIdx) {
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      nextOldFiber = oldFiber.sibling;
    }
    //  1.2 updateSlot会对比key,如果key相同,内部会继续调用updateElement等比较type
    //       └─如果key 继续比较type
    //         └─type相同 复用fiber, newFiber不为null
    //         └─type不同 创建新的fiber节点,newFiber不为空,oldFiber不为空,但是newFiber.alternate为空,
    //       └─如果key不同 newFiber为null
    var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);

    if (newFiber === null) {
      if (oldFiber === null) {
        oldFiber = nextOldFiber;
      }
      //  1.2.1 key不同 break退出1对1顺序比较,保持newIdx位置,例如头两个节点key相同,直到第三个节点key不同
      break;
    }

    if (shouldTrackSideEffects) {
      //  1.2.2 key相同type不同,把旧fiber标记为删除。例如原来是<li key="b">现在是<p key="b">
      if (oldFiber && newFiber.alternate === null) {
        deleteChild(returnFiber, oldFiber);
      }
    }
    //  1.2.3 把newFiber标记为添加,和添加的位置
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

    if (previousNewFiber === null) {
      //  记录头节点,第一个新子节点
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }

    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }
  //  2. 一比一顺序比较后,如果新的子节点遍历完了,剩下的所有的旧子节点标记为删除
  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);

    if (getIsHydrating()) {
      var numberOfForks = newIdx;
      pushTreeFork(returnFiber, numberOfForks);
    }
    //  返回第一个节点,通过sibling.sibling.sibling...可以不断访问到兄弟节点
    //  返回头节点。“返回头节点”,返回所有新子节点的第一个节点。不论是新增的,复用的
    return resultingFirstChild;
  }
  //  3. 一比一顺序比较后,如果旧子节点没有了,但是新的子节点还有,创建对应的fiber,标记为添加
  if (oldFiber === null) {
    for (; newIdx < newChildren.length; newIdx++) {
      var _newFiber = createChild(returnFiber, newChildren[newIdx], lanes);

      if (_newFiber === null) {
        continue;
      }
      // 3.1 新的fiber标记为添加
      lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);

      if (previousNewFiber === null) {
        //  第一次循环 标记为第一个节点
        resultingFirstChild = _newFiber;
      } else {
        //  第二次 第三次...作为前一个节点的兄弟节点
        previousNewFiber.sibling = _newFiber;
      }
      //  第一次 第二次 第三次...缓存为前一个节点
      previousNewFiber = _newFiber;
    }

    if (getIsHydrating()) {
      var _numberOfForks = newIdx;
      pushTreeFork(returnFiber, _numberOfForks);
    }
    //  返回所有新子节点的第一个节点,通过sibling.sibling.sibling...可以不断访问到兄弟节点
    //  返回头节点。“返回头节点”,返回所有新子节点的第一个节点。不论是新增的,复用的
    return resultingFirstChild;
  } // Add all children to a key map for quick lookups.

  //  4. 一对一顺序比较完了,旧子节点和新子节点都有剩余
  //  4.1 把剩下的旧子节点放入map中
  var existingChildren = mapRemainingChildren(returnFiber, oldFiber); // Keep scanning and use the map to restore deleted items as moves.

  for (; newIdx < newChildren.length; newIdx++) {
    //  4.2 看能否复用旧的fiber, updateFromMap内部一样会判断key,内部继续调用updateElement等继续判断type,像updateSlot
    var _newFiber2 = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);
    //  4.2.1 key相同
    if (_newFiber2 !== null) {
      if (shouldTrackSideEffects) {
        //  4.2.2 type也相同,只有key type都相同,updateFromMap内部才会复用旧fiber构建alternate,key相同type不同创建新fiber,不会给alternate赋值
        if (_newFiber2.alternate !== null) {
          //  4.2.3 完全复用了旧fiber,从剩余的旧子节点中移除它
          existingChildren.delete(_newFiber2.key === null ? newIdx : _newFiber2.key);
        }
      }
      //  4.2.4 新fiber标记为添加
      lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);

      if (previousNewFiber === null) {
        resultingFirstChild = _newFiber2;
      } else {
        previousNewFiber.sibling = _newFiber2;
      }

      previousNewFiber = _newFiber2;
    }
  }

  if (shouldTrackSideEffects) {
    //  4.2.5 旧fiber不能复用,例如key相同type不同,把旧fiber标记为删除
    existingChildren.forEach(function (child) {
      return deleteChild(returnFiber, child);
    });
  }

  if (getIsHydrating()) {
    var _numberOfForks2 = newIdx;
    pushTreeFork(returnFiber, _numberOfForks2);
  }
  //  返回第一个节点,通过sibling.sibling.sibling...可以不断访问到兄弟节点
  //  返回头节点。“返回头节点”,返回所有新子节点的第一个节点。不论是新增的,复用的
  return resultingFirstChild;
}

写在最后

源码中除了主要的三个for循环,placeChild是同样重要的细节,每一轮for都有一个placeChild,给每个Fiber打上插入(Placement)移动(Placement)删除(Deletion)标记。