React的更新流程

对segmentfault上的大佬的文章进行一下总结,想看更详细的建议看这个专栏。

React加入了fiber之后,树的结构发生了改变,用以前的从上至下的单向树,改成了双向链表。也就是Fiber节点中加入了对Parent的引用。

更新流程:
  React的更新分为Render和Commit两个阶段
  React Fiber节点的更新在React的Render阶段,分为beginWork, completeWork两个阶段
  首先,React从上往下遍历fiber树,对于每一个fiber节点触发beginWork阶段,这个阶段会根据setState的调用形成的updateList和本次调度的优先级,来进行state的更新,如果首次发现低于当前优先级的update,会跳过这个update,并且在fiber节点上记录这个被跳过的update之前的所有update计算生成的state,也就是baseState,然后记录firstBaseUpdate为这个跳过的update,记录lastBaseUpdate为updateList的最后一个,用于后续低优先级调度的时候计算完整的state。另外beginWork还会根据旧的fiber树,拦截不需要更新的fiber,并根据旧fiber进行新fiber(这棵新的树叫做Work In Progress树,简称WIP)的构建。构建过程中,会进行一个diff操作。
  上面说到新的state会被计算出来,这时候React会执行一次render,获取到新的ReactElement,然后把新的这些ReactElement和旧的Fiber节点进行对比,这里主要涉及到fiber节点的更新,删除,移动操作。首先,如果两个ReactElement的key + tag(tag就是元素的类型)不一样,那么直接用新节点生成Fiber替换旧节点的Fiber,子树也进行完全重建。然后diff分为两种(以下说的删除和更新Fiber,只是给Fiber打上对应的EffectTag,然后在CompleteWork进行收集):

  1. 如果新的ReactElement只有一个,那么这时候是单元素的diff。这时候找一下旧Fiber节点有没有对应的节点(也就是key + tag相同的节点),如果有对应的节点,那么执行节点更新,并删除旧fiber节点的其他节点,WIP只存在这个对应节点构建的新节点。如果没有对应的节点,删除旧fiber的所有节点,WIP上只存在这个新构建的新节点。
  2. 如果新的ReactElement有多个,那么这时候,我们需要根据key来判断节点有没有移动。对新节点和旧节点进行逐一对比,如果发现相同,判断属性需不需要更新,需要则进行更新,更新完毕构建一个新的Fiber到WIP上。如果发现某一个的key + tag对不上了,那么以上一个相同的节点作为基准节点(用来判断位置),如果后面发现这个对不上的节点只是位置移动了,那么根据基准节点移动位置,同时判断属性是否需要更新,然后构建一个新的Fiber到WIP上。如果新的ReactElement全部遍历完了,但是旧Fiber节点还剩下,表示剩下的旧Fiber已经没用了,那么删除剩下的所有旧Fiber节点。如果旧Fiber节点全部遍历完了,但是还有新的ReactElement,表示有新的元素加入,那么构建新的Fiber到WIP上。

  diff过程中,会根据Fiber的状态打上effectTag,例如被删除的Fiber打上Deletion的EffectTag,之后在completeWork阶段进行收集工作。经过diff之后,新Fiber的形态基本确定,这时候继续深度优先遍历旧Fiber的子树,对每个子节点进行beginWork。
  当深度优先遍历的一个路径上的最后一个叶子节点进入beginWork并结束,开始回溯,让回溯过程中的每一个fiber节点进入completeWork阶段。如果回溯过程中有没进入beginWork的节点,那么接着进入beginWork。
  completeWork阶段,完成的事项有:
  1. 对Dom进行属性更新,或者创建。
  2. 如果Dom有创建,把第一层子Fiber关联到当前Fiber上。
  3. 收集effectList,最后交给Commit阶段处理。
  先来看看1,在completeWork阶段,会根据WIP的新Fiber节点的tag去处理不同的Dom更新,如果是实际的Dom Fiber(在源码中为HostComponent和HostText等),这时候会判断Fiber的Dom是否存在,如果存在,那么执行Dom的更新。如果不存在,那么执行Dom的创建(只是更新的话,新Fiber的Dom来自旧Fiber的Dom,如果是新节点,那么肯定是空)。
  然后是第2点,当对Dom创建完毕之后,会去把这个Fiber的第一层的子节点关联到这个Fiber的Dom的children这里。这里注意的是,如果是自定义组件Fiber,是不会出现关联子节点的,因为自定义组件的Fiber只是用来标记JSX的结构,不会有真实的dom。自定义组件的Fiber就是: -> (

'hhhh'),这时候从'hhhh'的textNode回溯到Example,发现Example是个自定义组件,不需要执行挂载操作,接着找Example的兄弟节点。在从当前Fiber找子节点的过程中,也会跳过自定义组件Fiber,直到找到真实Dom Fiber才会执行关联子节点操作。
  接着是第3点,在对Dom进行更新,创建操作之后,会把当前的Fiber节点标记一个effectTag。源码大概就是(相对于原文章的说法,React更改了名称为flags):

function markUpdate(workInProgress: Fiber) {
  // Tag the fiber with an update effect. This turns a Placement into
  // a PlacementAndUpdate.
  workInProgress.flags |= Update;
}

一旦WIP节点上有了除NoEffects之外的effect的话,表示这个节点需要在commit阶段被处理。每个Fiber节点都有firstEffect和lastEffect这两个指针,表示子节点中需要被处理的节点。如果当前节点没有更新,那么把自己的子节点的effectList并入到自己父节点的effectList中即可。如果当前节点有更新,那么在把自己子节点的effectList并入到自己父节点的effectList之后,再把自己也加入到父节点的effectList中。以此达到effectList收集的目的。从这个过程来看,最终只需要读取根节点的effectList,即可以对整个视图进行更新。贴一下来自大佬文章的代码:

 /*
* effectList是一条单向链表,每完成一个工作单元上的任务,
* 都要将它产生的effect链表并入
* 上级工作单元。
* */
// 将当前节点的effectList并入到父节点的effectList
if (returnFiber.firstEffect === null) {
  returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
  if (returnFiber.lastEffect !== null) {
    returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
  }
  returnFiber.lastEffect = completedWork.lastEffect;
}

// 将自身添加到effect链,添加时跳过NoWork 和
// PerformedWork的effectTag,因为真正
// 的commit用不到
const effectTag = completedWork.effectTag;

if (effectTag > PerformedWork) {
  if (returnFiber.lastEffect !== null) {
    returnFiber.lastEffect.nextEffect = completedWork;
  } else {
    returnFiber.firstEffect = completedWork;
  }
  returnFiber.lastEffect = completedWork;
}

你可能感兴趣的:(React的更新流程)