react状态更新

本文详细解析React状态更新流程,涵盖render阶段开始、commit阶段、Update对象创建、从fiber到root、Update分类、Update结构、Fiber与Update的关系,以及ReactDOM.render、setState和forceUpdate的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

React状态更新

几个关键节点

在开始学习前,我们先了解源码中几个关键节点(即几个关键函数的调用)。通过这章的学习,我们会将这些关键节点的调用路径串起来。

先从我们所熟知的概念开始。

render阶段的开始

render阶段开始于performSyncWorkOnRootperformConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。

commit阶段的开始

commit阶段开始于commitRoot方法的调用。其中rootFiber会作为传参。

我们已经知道,render阶段完成后会进入commit阶段。让我们继续补全从触发状态更新render阶段的路径。

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

    |
    |
    v

    ?

    |
    |
    v

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

    |
    |
    v

commit阶段(`commitRoot`)

创建Update对象

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

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

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

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

我们会在下一节详细讲解Update

从fiber到root

现在触发状态更新的fiber上已经包含Update对象。

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

答案是:调用markUpdateLaneFromFiberToRoot方法。

你可以从这里 (opens new window)看到markUpdateLaneFromFiberToRoot的源码

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

由于不同更新优先级不尽相同,所以过程中还会更新遍历到的fiber的优先级。

调度更新

现在我们拥有一个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)
  );
}

你可以从这里 (opens new window)看到ensureRootIsScheduled的源码

其中,scheduleCallbackscheduleSyncCallback会调用Scheduler提供的调度方法根据优先级调度回调函数执行。

可以看到,这里调度的回调函数为:

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

render阶段的入口函数。

至此,状态更新就和我们所熟知的render阶段连接上了。

Update的分类

我们先来了解Update的结构。

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

  • ReactDOM.render —— HostRoot
  • this.setState —— ClassComponent
  • this.forceUpdate —— ClassComponent
  • useState —— FunctionComponent
  • useReducer —— FunctionComponent

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

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

虽然他们的结构不同,但是他们工作机制与工作流程大体相同。

Update的结构

ClassComponentHostRoot(即rootFiber.tag对应类型)共用同一种Update结构

对应的结构如下:

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

  next: null,
};

UpdatecreateUpdate方法返回,你可以从这里 (opens new window)看到createUpdate的源码

字段意义如下:

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

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

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

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

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

  • callback:更新的回调函数。

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

Update与Fiber的联系

我们发现,Update存在一个连接其他Update形成链表的字段next。联系React中另一种以链表形式组成的结构Fiber,他们之间有什么关联么?

答案是肯定的。

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

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

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

什么情况下一个Fiber节点会存在多个Update?

你可能疑惑为什么一个Fiber节点会存在多个Update。这其实是很常见的情况。

在这里介绍一种最简单的情况:

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

  this.setState({
    b: 2
  })
}

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

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

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

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

updateQueue

ClassComponentHostRoot使用的UpdateQueue结构如下:

const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
      pending: null,
    },
    effects: null,
  };

UpdateQueueinitializeUpdateQueue方法返回,你可以从这里 (opens new window)看到initializeUpdateQueue的源码

字段说明如下:

  • baseState:本次更新前该Fiber节点stateUpdate基于该state计算更新后的state

  • firstBaseUpdatelastBaseUpdate:本次更新前该Fiber节点已保存的Update。以链表形式存在,链表头为firstBaseUpdate,链表尾为lastBaseUpdate。之所以在更新产生前该Fiber节点内就存在Update,是由于某些Update优先级较低所以在上次render阶段Update计算state时被跳过。

  • shared.pending:触发更新时,产生的Update会保存在shared.pending中形成单向环状链表。当由Update计算state时这个环会被剪开并连接在lastBaseUpdate后面。

  • effects:数组。保存update.callback !== nullUpdate

例子

updateQueue相关代码逻辑涉及到大量链表操作,比较难懂。在此我们举例对updateQueue的工作流程讲解下。

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

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

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

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

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

fiber.updateQueue.baseUpdate: u1 --> u2

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

每个 update 都会通过 enqueueUpdate 方法插入到 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,你可以在这里 (opens new window)看到enqueueUpdate的源码

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

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

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

接下来遍历updateQueue.baseUpdate链表,以fiber.updateQueue.baseState初始state,依次与遍历到的每个Update计算并产生新的state(该操作类比Array.prototype.reduce)。

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

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

render阶段Update操作processUpdateQueue完成,你可以从这里 (opens new window)看到processUpdateQueue的源码

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

渲染完成后workInProgress Fiber树变为current Fiber树,整个更新流程结束。

下面完整的走通ReactDOM.render完成页面渲染的整个流程。

创建fiber

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

这一步发生在调用ReactDOM.render后进入的legacyRenderSubtreeIntoContainer方法中。

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

你可以从这里 (opens new window)看到这一步的代码

legacyCreateRootFromDOMContainer方法内部会调用createFiberRoot方法完成fiberRootNoderootFiber的创建以及关联。并初始化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;
}

补充上rootFiberfiberRootNode的引用。

fiberRoot

你可以从这里 (opens new window)看到这一步的代码

创建update

我们已经做好了组件的初始化工作,接下来就等待创建Update来开启一次更新。

这一步发生在updateContainer方法中。

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);

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

你可以从这里 (opens new window)看到updateContainer的代码

值得注意的是其中update.payload = {element};

对于HostRootpayloadReactDOM.render的第一个传参。

流程概览

至此,ReactDOM.render的流程就和我们已知的流程连接上了。

整个流程如下:

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

    |
    |
    v

创建Update对象(`updateContainer`)

    |
    |
    v

从fiber到root(`markUpdateLaneFromFiberToRoot`)

    |
    |
    v

调度更新(`ensureRootIsScheduled`)

    |
    |
    v

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

    |
    |
    v

commit阶段(`commitRoot`)

this.setState

当我们有了前面知识的铺垫,就很容易理解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');
};

你可以在这里 (opens new window)看到这段代码

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);
}

你可以在这里 (opens new window)看到enqueueSetState代码

这里值得注意的是对于ClassComponentupdate.payloadthis.setState的第一个传参(即要改变的state)。

this.forceUpdate

this.updater上,除了enqueueSetState外,还存在enqueueForceUpdate,当我们调用this.forceUpdate时会调用他。

可以看到,除了赋值update.tag = ForceUpdate;以及没有payload外,其他逻辑与this.setState一致。

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);
  },
};

你可以在这里 (opens new window)看到enqueueForceUpdate代码

那么赋值update.tag = ForceUpdate;有何作用呢?

在判断ClassComponent是否需要更新时有两个条件需要满足:

 const shouldUpdate =
  checkHasForceUpdateAfterProcessing() ||
  checkShouldComponentUpdate(
    workInProgress,
    ctor,
    oldProps,
    newProps,
    oldState,
    newState,
    nextContext,
  );

你可以在这里 (opens new window)看到这段代码

  • checkHasForceUpdateAfterProcessing:内部会判断本次更新的Update是否为ForceUpdate。即如果本次更新的Update中存在tagForceUpdate,则返回true
  • checkShouldComponentUpdate:内部会调用shouldComponentUpdate方法。以及当该ClassComponentPureComponent时会浅比较stateprops

你可以在这里 (opens new window)看到checkShouldComponentUpdate代码

所以,当某次更新含有tagForceUpdateUpdate,那么当前ClassComponent不会受其他性能优化手段shouldComponentUpdate|PureComponent)影响,一定会更新。

cessing:内部会判断本次更新的Update是否为ForceUpdate。即如果本次更新的Update中存在tagForceUpdate,则返回true

  • checkShouldComponentUpdate:内部会调用shouldComponentUpdate方法。以及当该ClassComponentPureComponent时会浅比较stateprops

你可以在这里 (opens new window)看到checkShouldComponentUpdate代码

所以,当某次更新含有tagForceUpdateUpdate,那么当前ClassComponent不会受其他性能优化手段shouldComponentUpdate|PureComponent)影响,一定会更新。

至此,我们学习完了HostRoot | ClassComponent所使用的Update的更新流程。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值