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里相同的节点删除,这一轮遍历是根据新集合的顺序
相关推荐
学习使我快乐0116 分钟前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio199516 分钟前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈1 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水2 小时前
简洁之道 - React Hook Form
前端
正小安4 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch6 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光6 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   6 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   6 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web6 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery