react-类组件状态更新流程

loading 2024年01月19日 155次浏览

前两篇文章分别简要说明了render阶段和commit阶段的流程,那么什么会触发render呢?显然是组件中的状态更新,包含以下方法:

  • ReactDOM.render

  • this.setState

  • this.forceUpdate

  • useState

  • useReducer

这些方法调用的场景各不相同,他们是如何接入同一套状态更新机制呢?

1. 概览

1.1 创建Update对象

上面的问题的答案是:每次状态更新都会创建一个保存更新状态相关内容的对象,我们叫他Update,这个对象保存在触发状态更新的fiber中。在render阶段的beginWork中会根据Update计算新的state。

1.2 从触发更新的fiber到root

我们知道,beginWork是从rootFiber开始向下遍历。那么是如何从触发状态更新的fiber回到rootFiber呢?

答案是:调用markUpdateLaneFromFiberToRoot方法。

该方法做的工作可以概括为:从触发状态更新的fiber一直向上遍历到rootFiber,并返回rootFiber

由于不同更新优先级不尽相同,所以过程中还会更新遍历到的fiber的优先级。这目前属于超纲内容。

1.2 调度更新

上一步完成后我们找到了rootFiber,该rootFiber对应的Fiber树中某个Fiber节点包含一个Update

接下来通知Scheduler根据更新的优先级,决定以同步还是异步的方式调度本次更新。

这里调用的方法是ensureRootIsScheduled

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

留意这段代码可以发现这两句:

performSyncWorkOnRoot.bind(null, root);
performConcurrentWorkOnRoot.bind(null, root);

看起来很熟悉吧,因为这就是render环节的入口函数。这样一来,我们就知道状态更新是怎么触发render的了。

2. Update

2.1 分类

首先,我们将可以触发更新的方法所隶属的组件分类:

  • ReactDOM.render —— HostRoot

  • this.setState —— ClassComponent

  • this.forceUpdate —— ClassComponent

  • useState —— FunctionComponent

  • useReducer —— FunctionComponent

可以看到,一共三种组件(HostRoot | ClassComponent | FunctionComponent)可以触发更新。

由于不同类型组件工作方式不同,所以存在两种不同结构的Update,其中ClassComponent与HostRoot共用一套Update结构,FunctionComponent单独使用一种Update结构。

虽然他们的结构不同,但是他们工作机制与工作流程大体相同。在本节我们介绍前一种Update,FunctionComponent对应的Update在Hooks章节介绍。

2.2 结构

第一种Update的结构如下:

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

  next: null,
};

Update由createUpdate方法返回

字段意义如下:

  • eventTime:任务时间,通过performance.now()获取的毫秒数。由于该字段在未来会重构,当前我们不需要理解他。

  • lane:优先级相关字段。当前还不需要掌握他,只需要知道不同Update优先级可能是不同的。

  • suspenseConfig:Suspense相关,暂不关注。

  • tag:更新的类型,包括UpdateState | ReplaceState | ForceUpdate | CaptureUpdate

  • payload:更新挂载的数据,不同类型组件挂载的数据不同。对于ClassComponent,payload为this.setState的第一个传参。对于HostRoot,payload为ReactDOM.render的第一个传参。

  • callback:更新的回调函数。即在commit阶段的 layout 子阶段一节中提到的回调函数

next:与其他Update连接形成链表。

2.3 和Fiber的联系

从参数中我们能留意到Update存在一个连接其他Update形成链表的字段next。联系React中另一种以链表形式组成的结构Fiber,他们存在一定的关联。

类似Fiber节点组成Fiber树,Fiber节点上的多个Update会组成链表并被包含在fiber.updateQueue中。

举个例子:

onClick() {
  this.setState({
    a: 1
  })

  this.setState({
    b: 2
  })
}

在一个ClassComponent中触发onClick方法,方法内部调用了两次this.setState。这会在该fiber中产生两个Update。

Fiber节点最多同时存在两个updateQueue:

  • current fiber保存的current updateQueue
  • workInProgress fiber保存的workInProgress updateQueue

3. Update Queue

3.1 结构

ClassComponentHostRoot使用的UpdateQueue结构如下:

const queue: UpdateQueue<State> = {
    baseState: fiber.memorizedState,
    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。

3.2 整体流程

来举一个例子,看看整体的状态更新到render的流程是怎么连接起来的。

假设有一个fiber刚经历commit阶段完成渲染。

该fiber上有两个由于优先级过低所以在上次的render阶段并没有处理的Update。他们会成为下次更新的baseUpdate。

我们称其为u1和u2,其中u1.next === u2。

fiber.updateQueue.firstBaseUpdate === u1;
fiber.updateQueue.lastBaseUpdate === u2;
u1.next === u2;

现在我们在fiber上触发两次状态更新,这会先后产生两个新的Update,我们称为u3和u4。

每个 update 都会通过 enqueueUpdate 方法插入到 updateQueue 队列上。

fiber.updateQueue.shared.pending:   u4 ──> u3
                                     ^      |                                    
                                     └──────┘

更新调度完成后进入render阶段。

此时shared.pending的环被剪开并连接在updateQueue.lastBaseUpdate后面:

fiber.updateQueue.baseUpdate: u1 --> u2 --> u3 --> u4

接下来遍历updateQueue.baseUpdate链表,以baseState为初始state,依次与遍历到的每个Update计算并产生新的state(该操作类似于数组的reduce方法)。

在遍历时如果有优先级低的Update会被跳过。

当遍历完成后获得的state,就是该Fiber节点在本次更新的state(源码中叫做memorizedState)。

render阶段的Update操作由processUpdateQueue完成

state的变化在render阶段产生与上次更新不同的JSX对象,通过Diff算法产生effectTag,在commit阶段渲染在页面上。

dom节点渲染完成后(commit中的mutation阶段结束后)workInProgress Fiber树变为current Fiber树,整个更新流程结束。

来看最终的流程图:

创建fiberRootNode、rootFiber、updateQueue(`legacyCreateRootFromDOMContainer`)

    |
    |
    v

创建Update对象(`updateContainer`)

    |
    |
    v

从fiber到root(`markUpdateLaneFromFiberToRoot`)

    |
    |
    v

调度更新(`ensureRootIsScheduled`)

    |
    |
    v

render阶段(`performSyncWorkOnRoot` 或 `performConcurrentWorkOnRoot`)

    |
    |
    v

commit阶段(`commitRoot`)