react源码中的reconcileChildrenArray函数是如何对节点进行diff的

reconcileChildrenArray是react对于多节点diff进行操作的函数

在了解diff之前先需要了解一下fiber

fiber

在react中通过jsx生成虚拟dom对象,然后通过虚拟dom对象生产fiber对象,最后通过fiber生产视图。

大致流程

jsx->vdom->fiber

源码在react/src/ReactFiber.old.js

js 复制代码
function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  ...todo
}

fiber是链表结构,反应的真实dom的位置关系。 以下几个属性是和本文比较有关系。

  • key 节点的唯一标识符,用于进行节点的比较更新。就是在jsx标签中写的key
  • elementType 大部分情况与 type 相同,某些情况不同,比如 FunctionComponent 使用 React.memo 包裹,表示元素的类型
  • type 表示元素的类型, 对于 FunctionComponent,指函数本身,对于ClassComponent,指 class,对于 HostComponent,指 DOM 节点 tagName
  • stateNode FiberNode 对应的真实 DOM 节点
  • return 指向该 FiberNode 的父节点
  • child 指向该 FiberNode 的第一个子节点
  • sibling 指向右边第一个兄弟 Fiber 节点
  • index 当前节点在父节点的子节点列表中的索引
jsx 复制代码
function App() {
  const [list, setlist] = useState([
    { name: "zhangsan", id: 'a' },
    { name: "lisi", id: 'b' },
    { name: "wawngwu", id: 'c' },
  ]);
  return (
    <p>
      {list.map((item, index) => (
        <span key={item.id}>{item.name}</span>
      ))}
    </p>
  );
}

export default App;

代码对应草图

了解完fiber接下来要讲的就是多节点diff了,这里的代码在核心在react/src/ReactChildFiber.old.jsreconcileChildrenArray函数中

jsx 复制代码
if (isArray(newChild)) {
       return reconcileChildrenArray(
         //workInProgress tree
         returnFiber,
         //生成workInProgress tree的current tree的child,参考上图的关系
         currentFirstChild,
         //新生成的虚拟dom
         newChild,
         lanes,
       );
     }

returnFiber是需要更新的oldfiberworkInProgress(这里需要说一下在更新的时候每个fiber对象都会创建workInProgress对象,就相当于fiber的镜像。后续所有的更新都在workInProgress上,更新完成之后workInProgress就代表最后dom的状态)

jsx 复制代码
 function reconcileChildrenArray(
   returnFiber: Fiber,
   currentFirstChild: Fiber | null,
   newChildren: Array<*>,
   lanes: Lanes,
 ): Fiber | null {
  //作为子链表的第一个fiber的返回
   //最后return resultingFirstChild
   let resultingFirstChild: Fiber | null = null;
   //储存上一次操作的fiber
   let previousNewFiber: Fiber | null = null;

   let oldFiber = currentFirstChild;
   //被更新的节点
   let lastPlacedIndex = 0;
   //遍历的索引
   let newIdx = 0;
   //下一个要操作的fiber
   let nextOldFiber = null;
 }
 

通过以下demo来演示多点diff过程

jsx 复制代码
function App() {
 useEffect(()=>{
setTimeout(()=>{
 setlist([
   
   { name: "zhangsan", id: 'a' },
   { name: "wawngwu", id: 'c' },
   { name: "lisi", id: 'b'},
 ])
},1000)
 },[])
 const [list, setlist] = useState([
   { name: "zhangsan", id: 'a' },
   { name: "lisi", id: 'b'},
   { name: "wawngwu", id: 'c' },
 ]);
 return (
   <p>
     {list.map((item, index) => (
       <span key={item.id}>{item.name}</span>
     ))}
   </p>
 );
}

export default App;

节点示意图如下

jsx 复制代码
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
     if (oldFiber.index > newIdx) {
       nextOldFiber = oldFiber;
       oldFiber = null;
     } else {
       //因为是数组,所以需要保存需要被操作的下一个节点内容
       nextOldFiber = oldFiber.sibling;
     }
     //两种情况
     //oldFiber.key === newChildren[newIdx]  返回更新后的节点
      //oldFiber.key !== newChildren[newIdx] 返回null
     const newFiber = updateSlot(
       returnFiber,
       oldFiber,
       newChildren[newIdx],
       lanes,
     );
     //代表本次key没有匹配到,直接跳出循环
     if (newFiber === null) {
       if (oldFiber === null) {
         oldFiber = nextOldFiber;
       }
       break;
     }
     if (shouldTrackSideEffects) {
      
       if (oldFiber && newFiber.alternate === null) {
         // We matched the slot, but we didn't reuse the existing fiber, so we
         // need to delete the existing child.
          //删除节点
         deleteChild(returnFiber, oldFiber);
       }
     } 
     //移动操作
     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;
     }
     //当前循环操作的fiber节点
     previousNewFiber = newFiber;
     //把之前储存的oldfiber.sibing作为本次循环的oldfiber
     oldFiber = nextOldFiber;
   }

lastPlacedIndex是一种顺序优化手段,lastPlacedIndex 一直在更新,它表示旧集合中在当前最大的值,也就是最靠近右边的位置,如果新集合的访问的fiber节点的index < lastPlacedIndex说明当前访问的节点比上一个节点靠后,不需要移动,否则的话需要移动。

第一轮遍历

oldFiber.index = 0

newIdx=0

nextOldFiber = fiber span(key=b)

进入updateSlotfiber span(key=a) newChildren[0]的比较

jsx 复制代码
 function updateSlot(
    returnFiber: Fiber,
    oldFiber: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
   const key = oldFiber !== null ? oldFiber.key : null;
    if (newChild.key === key) {
        //key相同的话返回更新过后的fiber节点
            return updateElement(returnFiber, oldFiber, newChild, lanes);
          } else {
            //不相同的话返回null
            return null;
          }
  ...
  }

第二轮遍历

oldFiber = fiber span(key=b)

oldFiber.index = 1

newIdx=1

nextOldFiber = fiber span(key= c)

进入updateSlotfiber span(key=b) newChildren[1]的比较

发现key不同,说明不是同一个节点,退出循环

jsx 复制代码
if (newFiber === null) {
     if (oldFiber === null) {
       oldFiber = nextOldFiber;
     }
     break;
   }

此时

oldFiber = fiber span(key=b)

oldFiber.index = 1

nextOldFiber = fiber span(key= c)

newChildren.length=3

进入下一个环节

jsx 复制代码
  //不匹配
    if (newIdx === newChildren.length) {
    
    }
    //不匹配
    //    if (oldFiber === null) {
    }
    //通过遍历oldfiber以及他的silbing得到一个键值为{key:fiber}的map对象
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    

得到oldfiber后继续遍历新集合

第二轮遍历

oldFiber = fiber span(key=b)

newChildren.length=3

jsx 复制代码
 for (; newIdx < newChildren.length; newIdx++) {
 //第一次遍历,发现map里有newChildren[newIdx].key的成员,返回新的fiberNode
  //第二次遍历,发现map里有newChildren[newIdx].key的成员,返回新的fiberNode
 const newFiber = updateFromMap(
       existingChildren,
       returnFiber,
       newIdx,
       newChildren[newIdx],
       lanes,
     );
      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.
           //获得新的fiber,把map里的对应map删除
           existingChildren.delete(
             newFiber.key === null ? newIdx : newFiber.key,
           );
         }
       }
       //newidx作为newFiber.index,代表fiber在链表中的顺序,
       lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
       if (previousNewFiber === null) {
         resultingFirstChild = newFiber;
       } else {
         //把兄弟节点连接起来
         previousNewFiber.sibling = newFiber;
       }
       previousNewFiber = newFiber;
     }
 
 }
 

经过两轮遍历,对于当前变化的节点就都已经处理完毕。

例子

jsx 复制代码
变化前  abcd
变化后  bcead

第一轮遍历, a !== b ,退出循环
创建`map`,

{
  a:fiberA,
  b:fiberB,
  c:fiberC,
  d:fiberD
}
此时
newIdx = 0
lastPlaceIndex = 0
newChildren = bcead

遍历`newChildren`

 ### 第一遍 
  map中发现b节点,此时`oldindex = 1`  , 更新`b节点workInprogress`,因为 `oldindex > lastPlaceIndex `所以要更新`lastPlaceIndex`的值,然后删除`map`里`b节点`
newIdx = 0
lastPlaceIndex = 1
newChildren = bcead
 
 ### 第二遍 
 map中发现b节点,此时`oldindex = 2`,更新`c节点workInprogress`,因为 `oldindex > lastPlaceIndex `所以要更新`lastPlaceIndex`的值,然后删除`map`里`c节点`
 newIdx = 1
lastPlaceIndex = 2
newChildren = cead


 
 ### 第三遍 
`map`没有发现`c节点`,需要创建`c节点workInprogress`,因为是新增节点,所以`lastPlaceIndex`不变,将`workInprogress`标记为Placement
 newIdx = 2
lastPlaceIndex = 2
newChildren = ead

 ### 第四遍 
`map`发现`a节点`,此时`oldindex = 1`,需要更新`a节点workInprogress`,因为 `oldindex < lastPlaceIndex `,所以需要移动节点,将`workInprogress`标记为Placement
 newIdx = 3
lastPlaceIndex = 2
newChildren = ad

 ### 第四遍 
`map`发现`d节点`,此时`oldindex = 3`,需要更新`d节点workInprogress`,因为 `oldindex > lastPlaceIndex `,所以要更新`lastPlaceIndex`
 newIdx = 4
lastPlaceIndex = 3
newChildren = d

通过diff,更新了三次节点,移动了一个节点,插入了一个节点

总结

多节点diff有以下两种情况

1.第一轮可以遍历完新集合或者老节点

1.newIdx === newChildren.length,只需要需要删除老节点

2.oldFiber === null 最后操作的一个fiber的silbing为null,继续遍历新集合,创建对应的新节点。

2.第一轮无法遍历完新集合和老节点

  1. 第一轮遍历,对集合进行遍历,使用key去比较当前操作的fiberNode的key,如果匹配的话则更新节点,不匹配的话跳出遍历,进行下一步操作

2. 第二轮遍历,对当前的fiber及其他的兄弟节点进行以{key:fiberNode}的形式,存储在map对象中,继续遍历剩余的集合。查询map中是否有相同的key,如果有的话进行节点复用,再根据lastPlaceIndex来决定节点是否需要移动,没有的话就创建新的fiberNode,每次遍历删除掉map里已经被更新的key。

  1. 经过第二轮遍历说明新节点已经更新/创建完成。map里剩余的就是需要被删除的节点,只需要遍历map,把workInProgress里相同的节点删除,这一轮遍历是根据新集合的顺序
相关推荐
崔庆才丨静觅20 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606121 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了21 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅21 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅21 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 天前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 天前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 天前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 天前
jwt介绍
前端
爱敲代码的小鱼1 天前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax