React的状态更新是怎样的?

436 阅读12分钟

1. 概览

有以下几个关键的节点:

  1. render阶段的开始 开始于performSyncWorkOnRootperformConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新,render结束完后会进入commit
  2. commit阶段的开始 开始于commitRoot方法的调用。其中rootFiber会作为传参

此时,状态为

触发状态更新(根据场景调用不同方法)

    |
    |
    v

    ?

    |
    |
    v

render阶段(`performSyncWorkOnRoot``performConcurrentWorkOnRoot`)

    |
    |
    v

commit阶段(`commitRoot`

1.1 创建Update对象

  • ReactDOM.render
  • this.setState
  • this.forceUpdate
  • useState
  • useReducer

Q:如何在调用场景不同的情况下,接入同一套状态管理机制?

A:在每次状态更新,都会创建保存一个更新状态的相关的对象,称为Update,在render的beginwork中会根据Update得到新的state。

1.2 从fiber到root

Q:render阶段是从rootFiber开始向下遍历。那么如何从触发状态更新的fiber得到rootFiber呢?

A:调用markUpdateLaneFromFiberToRoot方法。

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

1.3 调度更新

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

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

至此,状态更新的流程已经通了

触发状态更新(根据场景调用不同方法)

    |
    |
    v

创建Update对象(后面详解)

    |
    |
    v

从fiber到root(`markUpdateLaneFromFiberToRoot`)

    |
    |
    v

调度更新(`ensureRootIsScheduled`)

    |
    |
    v

render阶段(`performSyncWorkOnRoot``performConcurrentWorkOnRoot`)

    |
    |
    v

commit阶段(`commitRoot`

2. Update

2.1 Update思路

通过代码版本控制类比

  1. 同步更新 在没有代码版本控制前,我们在代码中逐步叠加功能。一切看起来井然有序,直到我们遇到了一个紧急线上bug(红色节点)

image.png

如果要修改,需要先将之前的代码提交;

所有通过ReactDOM.render创建的应用都是通过类似的方式更新状态,没有优先级,高优先级需要排在其他更新的后面

  1. 并发更新 如果有了代码版本控制,需要发布urgent变更,暂存当前已有的修改,在master上直接修复

image.png

修复后使用 git rebase 和分支连接,当前开发的分支是基于修复bug的最新的master分支

在React中,通过ReactDOM.createBlockingRoot和ReactDOM.createRoot创建的应用会采用并发的方式更新状态;

高优更新(红色节点)中断正在进行中的低优更新(蓝色节点),先完成render - commit流程;

待高优更新完成后,低优更新基于高优更新的结果重新更新;

2.2 Update的分类

触发方法组件
ReactDOM.renderHostRoot
this.setStateClassComponent
this.forceUpdateClassComponent
useStateFunctionComponent
useReducerFunctionComponent

共有三种组件支持Update:HostRoot 、ClassComponent、FunctionComponent,其中,ClassComponent与HostRoot共用一套Update结构,FunctionComponent单独使用一种Update结构

虽然他们的结构不同,但是工作机制与工作流程大体相同。在这里介绍前一种Update,FunctionComponent对应的Update在后面介绍。

2.3 Update结构

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连接形成链表。

2.4 Update与Fiber联系

Fiber节点组成Fiber树,页面中最多同时存在两颗Fiber树:

  • 代表当前页面状态的current Fiber树
  • 代表正在render阶段的workInProgress Fiber树

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

Q:什么情况下,一个Fiber节点会有多个Update

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

  this.setState({
    b: 2
  })
}

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

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

在commit阶段完成页面渲染后,workInProgress Fiber树变为current Fiber树,workInProgress Fiber树内Fiber节点的updateQueue就变成current updateQueue

2.5 updateQuene

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,可以将baseState类比心智模型中的master分支;
  • firstBaseUpdate与lastBaseUpdate:本次更新前该Fiber节点已保存的Update。以链表形式存在,链表头为firstBaseUpdate,链表尾为lastBaseUpdate。之所以在更新产生前该Fiber节点内就存在Update,是由于某些Update优先级较低所以在上次render阶段由Update计算state时被跳过,可以将baseUpdate类比心智模型中执行git rebase基于的commit(节点D);
  • shared.pending:触发更新时,产生的Update会保存在shared.pending中形成单向环状链表。当由Update计算state时这个环会被剪开并连接在lastBaseUpdate后面,可以将shared.pending类比心智模型中本次需要提交的commit(节点ABC)。
  • effects:数组。保存update.callback !== null的Update;

2.6 demo

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

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

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

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

我们用-->表示链表的指向:

fiber.updateQueue.baseUpdate: u1 --> u2

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

每个 update 都会插入到 updateQueue 队列上

当插入u3后:

fiber.updateQueue.shared.pending === u3;
u3.next === u3;

shared.pending的环状链表,用图表示为:

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

接着插入u4之后:

fiber.updateQueue.shared.pending = u4;
u4.next === u3;
u3.next === u4;

shared.pending是环状链表,用图表示为:

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

shared.pending 会保证始终指向最后一个插入的update

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

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

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

接下来遍历updateQueue.baseUpdate链表,以fiber.updateQueue.baseState为初始state,依次与遍历到的每个Update计算并产生新的state;

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

当遍历完成后获得的state,就是该Fiber节点在本次更新的state

state的变化在render阶段产生与上次更新不同的JSX对象,通过Diff算法产生effectTag,在commit阶段渲染在页面上,同时,渲染完成后workInProgress Fiber树变为current Fiber树,整个更新流程结束。

3. 深入理解优先级

3.1 什么是优先级

状态更新由用户交互产生,用户心里对交互执行顺序有个预期。React根据人机交互研究的结果中用户对交互的预期顺序为交互产生的状态更新赋予不同优先级

  • 生命周期方法:同步执行;
  • 受控的用户输入:比如输入框内输入文字,同步执行;
  • 交互事件:比如动画,高优先级执行;
  • 其他:比如数据请求,低优先级执行;

3.2 如何调度优先级

React会调用Scheduler提供的方法runWithPriority,该方法接收一个优先级常量与一个回调函数作为参数。回调函数会以优先级高低为顺序排列在一个定时器中并在合适的时间触发。

3.3 demo

image.png

以上有两个Update。我们将“关闭黑夜模式”产生的Update称为u1,输入字母“I”产生的Update称为u2

其中u1先触发并进入render阶段。其优先级较低,执行时间较长。此时:

fiber.updateQueue = {
  baseState: {
    blackTheme: true,
    text: 'H'
  },
  firstBaseUpdate: null,
  lastBaseUpdate: null
  shared: {
    pending: u1
  },
  effects: null
};

在u1完成render阶段前用户通过键盘输入字母“I”,产生了u2。u2属于受控的用户输入,优先级高于u1,于是中断u1产生的render阶段。

fiber.updateQueue.shared.pending === u2 ----> u1
                                     ^        |
                                     |________|
// 即
u2.next === u1;
u1.next === u2;

其中u2优先级高于u1。

接下来进入u2产生的render阶段。

processUpdateQueue方法中,shared.pending环状链表会被剪开并拼接在baseUpdate后面。

需要明确一点,shared.pending指向最后一个pending的update,所以实际执行时update的顺序为:

u1 -- u2

接下来遍历baseUpdate,处理优先级合适的Update(这一次处理的是更高优的u2);

由于u2不是baseUpdate中的第一个update,在其之前的u1由于优先级不够被跳过;

update之间可能有依赖关系,所以被跳过的update及其后面所有update会成为下次更新的baseUpdate。(即u1 -- u2);

最终u2完成render - commit阶段;

此时:

fiber.updateQueue = {
  baseState: {
    blackTheme: true,
    text: 'HI'
  },
  firstBaseUpdate: u1,
  lastBaseUpdate: u2
  shared: {
    pending: null
  },
  effects: null
};

在commit阶段结尾会再调度一次更新。在该次更新中会基于baseState中firstBaseUpdate保存的u1,开启一次新的render阶段。

最终结果:

fiber.updateQueue = {
  baseState: {
    blackTheme: false,
    text: 'HI'
  },
  firstBaseUpdate: null,
  lastBaseUpdate: null
  shared: {
    pending: null
  },
  effects: null
};

可以看到,u2执行了2次,相对应的render阶段的生命周期 componentWillXXX也会执行2次,这就是为什么这些生命周期会被标记为unsafe_;

Q:render阶段可能会被中断,如何保证updateQueue中的Update不会丢失?

在render阶段,shared.pending的环被剪开并连接在updateQueue.lastBaseUpdate后面。 实际上shared.pending会被同时连接在workInProgress updateQueue.lastBaseUpdate与current updateQueue.lastBaseUpdate后面。

当render阶段被中断后重新开始时,会基于current updateQueue克隆出workInProgress updateQueue。由于current updateQueue.lastBaseUpdate已经保存了上一次的Update,所以不会丢失

当commit阶段完成渲染,由于workInProgress updateQueue.lastBaseUpdate中保存了上一次的Update,所以 workInProgress Fiber树变成current Fiber树后也不会造成Update丢失

Q:如何保证状态依赖的连续性?

当某个Update由于优先级低而被跳过时,保存在baseUpdate中的不仅是该Update,还包括链表中该Update之后的所有Update。

考虑如下例子:

baseState: ''
shared.pending: A1 --> B2 --> C1 --> D2

其中字母代表该Update要在页面插入的字母,数字代表优先级,值越低优先级越高。

第一次render,优先级为1。

baseState: ''
baseUpdate: null
render阶段使用的Update: [A1, C1]
memoizedState: 'AC'

其中B2由于优先级为2,低于当前优先级,所以他及其后面的所有Update会被保存在baseUpdate中作为下次更新的Update(即B2 C1 D2)。

这么做是为了保持状态的前后依赖顺序。

第二次render,优先级为2。

baseState: 'A'
baseUpdate: B2 --> C1 --> D2
render阶段使用的Update: [B2, C1, D2]
memoizedState: 'ABCD'

此时,React能保证最终的状态一定和用户触发的交互一致,但是中间过程状态无法保证

4. ReactDOM.render

首次执行ReactDOM.render会创建fiberRootNode和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是要渲染组件所在组件树的根节点

// container指ReactDOM.render的第二个参数(即应用挂载的DOM节点)
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
  container,
  forceHydrate,
);
fiberRoot = root._internalRoot;

legacyCreateRootFromDOMContainer方法内部会调用createFiberRoot方法完成fiberRootNode和rootFiber的创建以及关联。并初始化updateQueue

export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
  // 创建fiberRootNode
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
  
  // 创建rootFiber
  const uninitializedFiber = createHostRootFiber(tag);

  // 连接rootFiber与fiberRootNode
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  // 初始化updateQueue
  initializeUpdateQueue(uninitializedFiber);

  return root;
}

4.1 创建Update

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  // ...省略与逻辑不相关代码

  // 创建update
  const update = createUpdate(eventTime, lane, suspenseConfig);
  
  // update.payload为需要挂载在根节点的组件
  update.payload = {element};

  // callback为ReactDOM.render的第三个参数 —— 回调函数
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }

  // 将生成的update加入updateQueue
  enqueueUpdate(current, update);
  // 调度更新
  scheduleUpdateOnFiber(current, lane, eventTime);

  // ...省略与逻辑不相关代码
}

所以完整流程为:

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

    |
    |
    v

创建Update对象(`updateContainer`)

    |
    |
    v

从fiber到root(`markUpdateLaneFromFiberToRoot`)

    |
    |
    v

调度更新(`ensureRootIsScheduled`)

    |
    |
    v

render阶段(`performSyncWorkOnRoot``performConcurrentWorkOnRoot`)

    |
    |
    v

commit阶段(`commitRoot`

4.2 React其他入口函数

当前React共有三种模式:

  • legacy,这是当前React使用的方式。当前没有计划删除本模式,但是这个模式可能不支持一些新功能;
  • blocking,开启部分concurrent模式特性的中间模式。目前正在实验中。作为迁移到concurrent模式的第一个步骤;
  • concurrent,面向未来的开发模式。我们之前讲的任务中断/任务优先级都是针对concurrent模式;

支持的程度为:

legacy 模式blocking 模式concurrent 模式
String Refs🚫**🚫**
Legacy Context🚫**🚫**
findDOMNode🚫**🚫**
Suspense
SuspenseList🚫
Suspense SSR + Hydration🚫
Progressive Hydration🚫
Selective Hydration🚫🚫
Cooperative Multitasking🚫🚫
Automatic batching of multiple setStates🚫*
Priority-based Rendering🚫🚫
Interruptible Prerendering🚫🚫
useTransition🚫🚫
useDeferredValue🚫🚫
Suspense Reveal "Train"🚫🚫

模式的变化影响整个应用的工作方式,所以无法只针对某个组件开启不同模式

基于此原因,可以通过不同的入口函数开启不同模式:

  • legacy -- ReactDOM.render(, rootNode)
  • blocking -- ReactDOM.createBlockingRoot(rootNode).render()
  • concurrent -- ReactDOM.createRoot(rootNode).render()

5. this.setState

this.setState 会调用 this.updater.enqueueSetState

Component.prototype.setState = function (partialState, callback) {
  if (!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) {
    {
      throw Error( "setState(...): takes an object of state variables to update or a function which returns an object of state variables." );
    }
  }
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

enqueueSetState就是我们从创建update到调度update

enqueueSetState(inst, payload, callback) {
  // 通过组件实例获取对应fiber
  const fiber = getInstance(inst);

  const eventTime = requestEventTime();
  const suspenseConfig = requestCurrentSuspenseConfig();

  // 获取优先级
  const lane = requestUpdateLane(fiber, suspenseConfig);

  // 创建update
  const update = createUpdate(eventTime, lane, suspenseConfig);

  update.payload = payload;

  // 赋值回调函数
  if (callback !== undefined && callback !== null) {
    update.callback = callback;
  }

  // 将update插入updateQueue
  enqueueUpdate(fiber, update);
  // 调度update
  scheduleUpdateOnFiber(fiber, lane, eventTime);
}

5.1 this.forceUpdate

在this.updater上,除了enqueueSetState外,还存在enqueueForceUpdate,在this.forceUpdate时调用;

enqueueForceUpdate(inst, callback) {
    const fiber = getInstance(inst);
    const eventTime = requestEventTime();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const lane = requestUpdateLane(fiber, suspenseConfig);

    const update = createUpdate(eventTime, lane, suspenseConfig);

    // 赋值tag为ForceUpdate
    update.tag = ForceUpdate;

    if (callback !== undefined && callback !== null) {
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  },
};

此时,当某次更新含有tag为ForceUpdate的Update,那么当前ClassComponent不会受其他性能优化手段(shouldComponentUpdate|PureComponent)影响,一定会更新。