浅谈 React Update

3 views

浅谈 React Update

在深入源码前,让我们先建立更新机制心智模型

1. 心智模型

1.1 同步更新的 React

我们可以将更新机制类比代码版本控制

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

同步更新的 React

为了修复这个bug,我们需要首先将之前的代码提交。

React中,所有通过ReactDOM.render创建的应用(其他创建应用的方式参考ReactDOM.render一节)都是通过类似的方式更新状态

即没有优先级概念,高优更新(红色节点)需要排在其他更新后面执行。

1.2 并发更新的React

当有了代码版本控制,有紧急线上bug需要修复时,我们暂存当前分支的修改,在master分支修复bug并紧急上线。

并发更新

bug修复上线后通过git rebase命令和开发分支连接上。开发分支基于修复bug的版本继续开发。

git rebase

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

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

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

2.Update的分类

我们先来了解Update的结构。

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

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

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

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

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

3. Update的结构

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

对应的结构如下:

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

你可以将Update类比心智模型中的一次commitUpdatecreateUpdate方法返回,你可以从这里看到createUpdate的源码

字段意义如下:

  • eventTime:任务时间,通过performance.now()获取的毫秒数。由于该字段在未来会重构,当前我们不需要理解他。
  • lane:优先级相关字段。当前还不需要掌握他,只需要知道不同Update优先级可能是不同的。
  • suspenseConfig:Suspense相关,暂不关注。
  • tag:更新的类型,包括UpdateState | ReplaceState | ForceUpdate | CaptureUpdate
  • payload:更新挂载的数据,不同类型组件挂载的数据不同。对于ClassComponentpayloadthis.setState的第一个传参。对于HostRootpayloadReactDOM.render的第一个传参。
  • callback:更新的回调函数。
  • next:与其他Update连接形成链表。

4. 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

5. updateQueue

updateQueue有三种类型,其中针对HostComponent的类型我们在completeWork一节介绍过。

剩下两种类型和Update的两种类型对应。

ClassComponentHostRoot使用的UpdateQueue结构如下:

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

字段说明如下:

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

你可以将baseState类比心智模型中的master分支

  • firstBaseUpdatelastBaseUpdate:本次更新前该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 !== nullUpdate

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

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

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

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

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