react源码系列之四 、状态更新

443 阅读5分钟

1 流程概览

1.1 创建Update触发render

在React中,有如下方法可以触发状态更新进入render阶段(排除SSR相关):

  • ReactDOM.render
  • this.setState
  • this.forceUpdate
  • useState
  • useReducer 每次调用这些方法都会创建一个保存更新状态相关内容的对象(Update),在render阶段的beginWork中会根据Update计算新的state。

1.2 从fiber到root

现在触发状态更新的fiber上已经包含Update对象,但render阶段是要传入root从整个应用根节点开始向下深度优先遍历,通过调用markUpdateLaneFromFiberToRoot方法可从触发状态更新的fiber得到rootFiber。该方法做的工作是从触发状态更新的fiber一直向上遍历到rootFiber,并返回rootFiber,且由于不同更新优先级不尽相同,所以过程中还会更新遍历到的fiber的优先级。 补充: rootFiber.stateNode = FiberRootNode rootFiber是当前应用(workInProgress fiber树)的根节点,tag是HostRoot

1.3 调度更新

现在我们拥有一个rootFiber,该rootFiber对应的Fiber树中某个Fiber节点包含一个Update。 接下来通知Scheduler根据更新的优先级,决定以同步还是异步的方式调度本次更新。 这里调用的方法是ensureRootIsScheduled。 以下是ensureRootIsScheduled最核心的一段代码:

if (newCallbackPriority === SyncLanePriority) {
  // 任务已经过期,需要同步执行render阶段
  newCallbackNode = scheduleSyncCallback(
    performSyncWorkOnRoot.bind(null, root)
  );
} else {
  // 根据任务优先级异步执行render阶段
  var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
    newCallbackPriority
  );
  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
  );
}

其中,scheduleCallback和scheduleSyncCallback会调用Scheduler提供的调度方法根据优先级调度回调函数执行。这里调度的回调函数为: performSyncWorkOnRoot.bind(null, root); performConcurrentWorkOnRoot.bind(null, root); 即render阶段的入口函数。 至此,状态更新就和我们所熟知的render阶段连接上了。

2 优先级与Update

  • 优先级是一个全局的概念,而update存在于触发更新的fiber节点上,

image.png update1是normal优先级,红色的update2是userBlocking优先级

  • 假如本次更新是userBlocking的优先级的更新,因为update1的优先级低于update2,所以不参与这次计算,则fiber的newState是baseState加update2;

  • 然后是normal的更新调度,因为update1是normal的优先级,所以会参与本次计算,而update2优先级更高,也会参与本次计算,则newState是baseState+update1+update2

  • 假设有这么一颗fiber树,在f节点上请求接口数据后调用this.setState触发更新,则是一个normal优先级的更新,这时候会从f节点调用markUpdateLaneFromFiberToRoot一直向上遍历直到fiberRootNode,此时就在FiberRootNode中保存了一个NormalPriority;接着会以NormalPriority来调度整个应用的根节点,此时整个应用中只有一个被调度的任务,它的优先级是NormalPriority,然后就调用NormalPriority的回调函数,这个函数即render阶段的入口,这样就进入了NormalPriority的render阶段,接着会从根节点开始向下深度优先遍历,以NormalPriority依次执行每个组件的diff; image.png

  • 因为只有f节点上有update,所以只会在f节点上计算新的state

  • 如果在计算的过程中,f组件又出现了一个新的更新,比如用户点击,则会产生一个userBlocking的更新,又会从f开始调用markUpdateLaneFromFiberToRoot向上遍历直到FiberRootNode并注册一个userBlocking Priority的调度;此时会打断正在进行的render阶段,重新开始从根节点向下深度优先遍历执行一个userBlocking Priority的更新。 image.png

3 Update的计算

3.1 Update的分类

  • 一共三种组件(HostRoot | ClassComponent | FunctionComponent)可以触发更新。
  • 由于不同类型组件工作方式不同,所以存在两种不同结构的Update,其中ClassComponent与HostRoot共用一套Update结构,FunctionComponent单独使用一种Update结构。

3.2 Update的结构

Update由createUpdate方法返回,ClassComponent与HostRoot(即rootFiber.tag对应类型)共用同一种Update结构:

const update: Update<*> = {
  eventTime,
  lane,
  suspenseConfig,
  tag: UpdateState,
  payload: null,
  callback: null,

  next: null,
};
  • eventTime:任务时间,通过performance.now()获取的毫秒数;
  • lane:优先级相关字段;
  • suspenseConfig:Suspense相关;
  • tag:更新的类型,包括UpdateState | ReplaceState | ForceUpdate | CaptureUpdate;
  • payload:更新挂载的数据,不同类型组件挂载的数据不同。对于ClassComponent,payload为this.setState的第一个传参。对于HostRoot,payload为ReactDOM.render的第一个传参;
  • callback:更新的回调函数。即在commit 阶段的 layout 子阶段一节中提到的回调函数;
  • next:与其他Update连接形成链表。(一个fiber可能存在多个update,如同时调用多次setState,或者在render阶段出现新的更新)

3.3 Update与Fiber的关系

  • fiber节点上又个属性updateQueue,对于hostComponent,在completeWork阶段会形成一个数组[要改变属性的key,对应的keyValue];
  • ClassComponent与HostRoot使用的UpdateQueue结构如下:
const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
      pending: null,
    },
    effects: null,
  };
  • baseState:本次更新前该Fiber节点的state,Update基于该state计算更新后的state。
  • firstBaseUpdate与lastBaseUpdate:本次更新前该Fiber节点已保存的Update。以链表形式存在,链表头为firstBaseUpdate,链表尾为lastBaseUpdate。之所以在更新产生前该Fiber节点内就存在Update,是由于某些Update优先级较低所以在上次render阶段由Update计算state时被跳过。
  • shared.pending:触发更新时,产生的Update会保存在shared.pending中形成单向环状链表。当由Update计算state时这个环会被剪开并连接在lastBaseUpdate后面。
  • effects:数组。保存update.callback !== null的Update。 后面的内容,update和优先级相关的处理逻辑比较复杂,现阶段先跳过。

4 ReactDOM.reder及this.setState触发更新的流程图

image.png