react源码学习笔记二

·  阅读 521

第六章 状态更新

流程概览

经过前几章的学习,我们终于有足够的前置知识理解状态更新的整个流程。

这一章我们看看几种常见的触发状态更新的方法是如何完成工作的。

#几个关键节点

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

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

#render阶段的开始

我们在render阶段流程概览一节讲到,

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

#commit阶段的开始

我们在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阶段连接上了。

#总结

让我们梳理下状态更新的整个调用路径的关键节点:

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

    |
    |
    v

创建Update对象(接下来三节详解)

    |
    |
    v

从fiber到root(`markUpdateLaneFromFiberToRoot`    |
    |
    v

调度更新(`ensureRootIsScheduled`    |
    |
    v

render阶段(`performSyncWorkOnRoot``performConcurrentWorkOnRoot`    |
    |
    v

commit阶段(`commitRoot`复制代码

#总结

本节我们了解了状态更新的整个流程。

在接下来三节中,我们会花大量篇幅讲解Update的工作机制,因为他是构成React concurrent mode的核心机制之一。

心智模型

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

在后面两节讲解源码时,我们会将代码与心智模型联系上,方便你更好理解。

#同步更新的React

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

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

流程1

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

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

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

#并发更新的React

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

流程2

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

流程3

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

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

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

接下来两节我们会从源码角度讲解这套并发更新是如何实现的。

Update

通过本章第一节学习,我们知道状态更新流程开始后首先会创建Update对象

本节我们学习Update的结构与工作流程。

你可以将Update类比心智模型中的一次commit

#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章节介绍。

#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优先级可能是不同的。

你可以将lane类比心智模型需求的紧急程度

  • suspenseConfig:Suspense相关,暂不关注。
  • tag:更新的类型,包括UpdateState | ReplaceState | ForceUpdate | CaptureUpdate
  • payload:更新挂载的数据,不同类型组件挂载的数据不同。对于ClassComponentpayloadthis.setState的第一个传参。对于HostRootpayloadReactDOM.render的第一个传参。
  • callback:更新的回调函数。即在commit 阶段的 layout 子阶段一节中提到的回调函数
  • 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

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

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

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

你可以将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

#例子

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树,整个更新流程结束。

深入理解优先级

通过更新的心智模型,我们了解到更新具有优先级

那么什么是优先级优先级以什么为依据?如何通过优先级决定哪个状态应该先被更新?

本节我们会详细讲解。

#什么是优先级

React理念一节我们聊到React将人机交互研究的结果整合到真实的UI中。具体到React运行上这是什么意思呢?

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

具体如下:

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

#如何调度优先级

我们在新的React结构一节讲到,React通过Scheduler调度任务。

具体到代码,每当需要调度任务时,React会调用Scheduler提供的方法runWithPriority

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

对于更新来讲,传递的回调函数一般为状态更新流程概览一节讲到的render阶段的入口函数

你可以在==unstable_runWithPriority== 这里 (opens new window)看到runWithPriority方法的定义。在这里 (opens new window)看到Scheduler对优先级常量的定义。

#例子

优先级最终会反映到update.lane变量上。当前我们只需要知道这个变量能够区分Update的优先级。

接下来我们通过一个例子结合上一节介绍的Update相关字段讲解优先级如何决定更新的顺序。

该例子来自React Core Team Andrew向网友讲解Update工作流程的推文(opens new window)

优先级如何决定更新的顺序

在这个例子中,有两个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”,产生了u2u2属于受控的用户输入,优先级高于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指向最后一个pendingupdate,所以实际执行时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阶段结尾会再调度一次更新。在该次更新中会基于baseStatefirstBaseUpdate保存的u1,开启一次新的render阶段

最终两次Update都完成后的结果如下:

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

我们可以看见,u2对应的更新执行了两次,相应的render阶段的生命周期勾子componentWillXXX也会触发两次。这也是为什么这些勾子会被标记为unsafe_

#如何保证状态正确

现在我们基本掌握了updateQueue的工作流程。还有两个疑问:

  • render阶段可能被中断。如何保证updateQueue中保存的Update不丢失?
  • 有时候当前状态需要依赖前一个状态。如何在支持跳过低优先级状态的同时保证状态依赖的连续性

我们分别讲解下。

#如何保证Update不丢失

上一节例子中我们讲到,在render阶段shared.pending的环被剪开并连接在updateQueue.lastBaseUpdate后面。

实际上shared.pending会被同时连接在workInProgress updateQueue.lastBaseUpdatecurrent updateQueue.lastBaseUpdate后面。

具体代码见这里(opens new window)

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

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

#如何保证状态依赖的连续性

当某个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'
复制代码

注意这里baseState并不是上一次更新的memoizedState。这是由于B2被跳过了。

即当有Update被跳过时,下次更新的baseState !== 上次更新的memoizedState

跳过B2的逻辑见这里(opens new window)

通过以上例子我们可以发现,React保证最终的状态一定和用户触发的交互一致,但是中间过程状态可能由于设备不同而不同。

ReactDOM.render

经过五章的学习,我们终于回到了React应用的起点。

这一节我们完整的走通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};

这就是我们在Update一节介绍的,对于HostRootpayloadReactDOM.render的第一个传参。

#流程概览

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

整个流程如下:

创建fiberRootNode、rootFiber、updateQueue(`legacyCreateRootFromDOMContainer`    |
    |
    v

创建Update对象(`updateContainer`    |
    |
    v

从fiber到root(`markUpdateLaneFromFiberToRoot`    |
    |
    v

调度更新(`ensureRootIsScheduled`    |
    |
    v

render阶段(`performSyncWorkOnRoot``performConcurrentWorkOnRoot`    |
    |
    v

commit阶段(`commitRoot`复制代码

#React的其他入口函数

当前React共有三种模式:

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

你可以从下表看出各种模式对特性的支持:

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

*:legacy模式在合成事件中有自动批处理的功能,但仅限于一个浏览器任务。非React事件想使用这个功能必须使用 unstable_batchedUpdates。在blocking模式和concurrent模式下,所有的setState在默认情况下都是批处理的。

**:会在开发中发出警告。

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

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

  • legacy -- ReactDOM.render(<App />, rootNode)
  • blocking -- ReactDOM.createBlockingRoot(rootNode).render(<App />)
  • concurrent -- ReactDOM.createRoot(rootNode).render(<App />)

你可以在这里 (opens new window)看到React团队解释为什么会有这么多模式

虽然不同模式的入口函数不同,但是他们仅对fiber.mode变量产生影响,对我们在流程概览中描述的流程并无影响。

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)影响,一定会更新。

#总结

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

在下一章我们会学习另一种数据结构的Update —— 用于HooksUpdate

第七章 Hooks

Hooks理念

注意

在开始本章学习前,你需要了解Hooks的基本用法。

如果你还未使用过Hooks,可以从官方文档 (opens new window)开始。

你可以从这里 (opens new window)了解Hooks的设计动机。作为一名框架使用者,了解设计动机对于我们日常开发就足够了。

但是,为了更好的理解Hooks源码架构,我们需要转换身份,以框架开发者的角度来看待Hooks设计理念

#从LOGO聊起

LOGO

React LOGO的图案是代表原子atom)的符号。世间万物由原子组成,原子类型属性决定了事物的外观与表现。

同样,在React中,我们可以将UI拆分为很多独立的单元,每个单元被称为Component。这些Component属性类型决定了UI的外观与表现。

讽刺的是,原子在希腊语中的意思为不可分割的indivisible),但随后科学家在原子中发现了更小的粒子 —— 电子(electron)。电子可以很好的解释原子是如何工作的。

React中,我们可以说ClassComponent是一类原子

但对于Hooks来说,与其说是一类原子,不如说他是更贴近事物运行规律电子

我们知道,React的架构遵循schedule - render - commit的运行流程,这个流程是React世界最底层的运行规律

ClassComponent作为React世界的原子,他的生命周期componentWillXXX/componentDidXXX)是为了介入React的运行流程而实现的更上层抽象,这么做是为了方便框架使用者更容易上手。

相比于ClassComponent的更上层抽象,Hooks则更贴近React内部运行的各种概念(state | context | life-cycle)。

作为使用React技术栈的开发者,当我们初次学习Hooks时,不管是官方文档还是身边有经验的同事,总会拿ClassComponent的生命周期来类比Hooks API的执行时机。

这固然是很好的上手方式,但是当我们熟练运用Hooks时,就会发现,这两者的概念有很多割裂感,并不是同一抽象层次可以互相替代的概念。

比如:替代componentWillReceivePropsHooks是什么呢?

可能有些同学会回答,是useEffect

  useEffect( () => {
    console.log('something updated');
  }, [props.something])
复制代码

但是componentWillReceiveProps是在render阶段执行,而useEffect是在commit阶段完成渲染后异步执行。

这篇文章可以帮你更好理解componentWillReceiveProps深入源码剖析componentWillXXX为什么UNSAFE(opens new window)

所以,从源码运行规律的角度看待Hooks,可能是更好的角度。这也是为什么上文说HooksReact世界的电子而不是原子的原因。

以上见解参考自React Core Team Dan在 React Conf2018的演讲(opens new window)

#总结

Concurrent ModeReact未来的发展方向,而Hooks是能够最大限度发挥Concurrent Mode潜力的Component构建方式。

正如Dan在React Conf 2018演讲结尾所说:你可以从ReactLOGO中看到这些围绕着核心电子飞行轨道Hooks可能一直就在其中。

极简Hooks实现

为了更好理解Hooks原理,这一节我们遵循React的运行流程,实现一个不到100行代码的极简useState Hook。建议对照着代码来看本节内容。

#工作原理

对于useState Hook,考虑如下例子:

function App() {
  const [num, updateNum] = useState(0);

  return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;
}
复制代码

可以将工作分为两部分:

  1. 通过一些途径产生更新更新会造成组件render
  2. 组件renderuseState返回的num为更新后的结果。

其中步骤1更新可以分为mountupdate

  1. 调用ReactDOM.render会产生mount更新更新内容为useStateinitialValue(即0)。
  2. 点击p标签触发updateNum会产生一次update更新更新内容为num => num + 1

接下来讲解这两个步骤如何实现。

#更新是什么

  1. 通过一些途径产生更新更新会造成组件render

首先我们要明确更新是什么。

在我们的极简例子中,更新就是如下数据结构:

const update = {
  // 更新执行的函数
  action,
  // 与同一个Hook的其他更新形成链表
  next: null
}
复制代码

对于App来说,点击p标签产生的updateactionnum => num + 1

如果我们改写下ApponClick

// 之前
return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;

// 之后
return <p onClick={() => {
  updateNum(num => num + 1);
  updateNum(num => num + 1);
  updateNum(num => num + 1);
}}>{num}</p>;
复制代码

那么点击p标签会产生三个update

#update数据结构

这些update是如何组合在一起呢?

答案是:他们会形成环状单向链表

调用updateNum实际调用的是dispatchAction.bind(null, hook.queue),我们先来了解下这个函数:

function dispatchAction(queue, action) {
  // 创建update
  const update = {
    action,
    next: null
  }

  // 环状单向链表操作
  if (queue.pending === null) {
    update.next = update;
  } else {
    update.next = queue.pending.next;
    queue.pending.next = update;
  }
  queue.pending = update;

  // 模拟React开始调度更新
  schedule();
}
复制代码

环状链表操作不太容易理解,这里我们详细讲解下。

当产生第一个update(我们叫他u0),此时queue.pending === null

update.next = update;u0.next = u0,他会和自己首尾相连形成单向环状链表

然后queue.pending = update;queue.pending = u0

queue.pending = u0 ---> u0
                ^       |
                |       |
                ---------
复制代码

当产生第二个update(我们叫他u1),update.next = queue.pending.next;,此时queue.pending.next === u0, 即u1.next = u0

queue.pending.next = update;,即u0.next = u1

然后queue.pending = update;queue.pending = u1

queue.pending = u1 ---> u0   
                ^       |
                |       |
                ---------
复制代码

你可以照着这个例子模拟插入多个update的情况,会发现queue.pending始终指向最后一个插入的update

这样做的好处是,当我们要遍历update时,queue.pending.next指向第一个插入的update

#状态如何保存

现在我们知道,更新产生的update对象会保存在queue中。

不同于ClassComponent的实例可以存储数据,对于FunctionComponentqueue存储在哪里呢?

答案是:FunctionComponent对应的fiber中。

我们使用如下精简的fiber结构:

// App组件对应的fiber对象
const fiber = {
  // 保存该FunctionComponent对应的Hooks链表
  memoizedState: null,
  // 指向App函数
  stateNode: App
};
复制代码

#Hook数据结构

接下来我们关注fiber.memoizedState中保存的Hook的数据结构。

可以看到,Hookupdate类似,都通过链表连接。不过Hook无环单向链表

hook = {
  // 保存update的queue,即上文介绍的queue
  queue: {
    pending: null
  },
  // 保存hook对应的state
  memoizedState: initialState,
  // 与下一个Hook连接形成单向无环链表
  next: null
}
复制代码

注意

注意区分updatehook的所属关系:

每个useState对应一个hook对象。

调用const [num, updateNum] = useState(0);updateNum(即上文介绍的dispatchAction)产生的update保存在useState对应的hook.queue中。

#模拟React调度更新流程

在上文dispatchAction末尾我们通过schedule方法模拟React调度更新流程。

function dispatchAction(queue, action) {
  // ...创建update
  
  // ...环状单向链表操作

  // 模拟React开始调度更新
  schedule();
}
复制代码

现在我们来实现他。

我们用isMount变量指代是mount还是update

// 首次render时是mount
isMount = true;

function schedule() {
  // 更新前将workInProgressHook重置为fiber保存的第一个Hook
  workInProgressHook = fiber.memoizedState;
  // 触发组件render
  fiber.stateNode();
  // 组件首次render为mount,以后再触发的更新为update
  isMount = false;
}
复制代码

通过workInProgressHook变量指向当前正在工作的hook

workInProgressHook = fiber.memoizedState;
复制代码

在组件render时,每当遇到下一个useState,我们移动workInProgressHook的指针。

workInProgressHook = workInProgressHook.next;
复制代码

这样,只要每次组件renderuseState的调用顺序及数量保持一致,那么始终可以通过workInProgressHook找到当前useState对应的hook对象。

到此为止,我们已经完成第一步。

  1. 通过一些途径产生更新更新会造成组件render

接下来实现第二步。

  1. 组件renderuseState返回的num为更新后的结果。

#计算state

组件render时会调用useState,他的大体逻辑如下:

function useState(initialState) {
  // 当前useState使用的hook会被赋值该该变量
  let hook;

  if (isMount) {
    // ...mount时需要生成hook对象
  } else {
    // ...update时从workInProgressHook中取出该useState对应的hook
  }

  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    // ...根据queue.pending中保存的update更新state
  }
  hook.memoizedState = baseState;

  return [baseState, dispatchAction.bind(null, hook.queue)];
}
复制代码

我们首先关注如何获取hook对象:

if (isMount) {
  // mount时为该useState生成hook
  hook = {
    queue: {
      pending: null
    },
    memoizedState: initialState,
    next: null
  }

  // 将hook插入fiber.memoizedState链表末尾
  if (!fiber.memoizedState) {
    fiber.memoizedState = hook;
  } else {
    workInProgressHook.next = hook;
  }
  // 移动workInProgressHook指针
  workInProgressHook = hook;
} else {
  // update时找到对应hook
  hook = workInProgressHook;
  // 移动workInProgressHook指针
  workInProgressHook = workInProgressHook.next;
}
复制代码

当找到该useState对应的hook后,如果该hook.queue.pending不为空(即存在update),则更新其state

// update执行前的初始state
let baseState = hook.memoizedState;

if (hook.queue.pending) {
  // 获取update环状单向链表中第一个update
  let firstUpdate = hook.queue.pending.next;

  do {
    // 执行update action
    const action = firstUpdate.action;
    baseState = action(baseState);
    firstUpdate = firstUpdate.next;

    // 最后一个update执行完后跳出循环
  } while (firstUpdate !== hook.queue.pending.next)

  // 清空queue.pending
  hook.queue.pending = null;
}

// 将update action执行完后的state作为memoizedState
hook.memoizedState = baseState;
复制代码

完整代码如下:

function useState(initialState) {
  let hook;

  if (isMount) {
    hook = {
      queue: {
        pending: null
      },
      memoizedState: initialState,
      next: null
    }
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook;
    } else {
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;
  } else {
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }

  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    let firstUpdate = hook.queue.pending.next;

    do {
      const action = firstUpdate.action;
      baseState = action(baseState);
      firstUpdate = firstUpdate.next;
    } while (firstUpdate !== hook.queue.pending.next)

    hook.queue.pending = null;
  }
  hook.memoizedState = baseState;

  return [baseState, dispatchAction.bind(null, hook.queue)];
}
复制代码

#对触发事件进行抽象

最后,让我们抽象一下React的事件触发方式。

通过调用App返回的click方法模拟组件click的行为。

function App() {
  const [num, updateNum] = useState(0);

  console.log(`${isMount ? 'mount' : 'update'} num: `, num);

  return {
    click() {
      updateNum(num => num + 1);
    }
  }
}
复制代码

#在线Demo

至此,我们完成了一个不到100行代码的Hooks。重要的是,他与React的运行逻辑相同。

精简Hooks的在线Demo

调用window.app.click()模拟组件点击事件。

你也可以使用多个useState

function App() {
  const [num, updateNum] = useState(0);
  const [num1, updateNum1] = useState(100);

  console.log(`${isMount ? 'mount' : 'update'} num: `, num);
  console.log(`${isMount ? 'mount' : 'update'} num1: `, num1);

  return {
    click() {
      updateNum(num => num + 1);
    },
    focus() {
      updateNum1(num => num + 3);
    }
  }
}
复制代码

#与React的区别

我们用尽可能少的代码模拟了Hooks的运行,但是相比React Hooks,他还有很多不足。以下是他与React Hooks的区别:

  1. React Hooks没有使用isMount变量,而是在不同时机使用不同的dispatcher。换言之,mount时的useStateupdate时的useState不是同一个函数。
  2. React Hooks有中途跳过更新的优化手段。
  3. React HooksbatchedUpdates,当在click中触发三次updateNum精简React会触发三次更新,而React只会触发一次。
  4. React Hooksupdate优先级概念,可以跳过不高优先的update

更多的细节,我们会在本章后续小节讲解。

Hooks的数据结构

在上一节我们实现了一个极简的useState,了解了Hooks的运行原理。

本节我们讲解Hooks的数据结构,为后面介绍具体的hook打下基础。

#dispatcher

在上一节的极简useState实现中,使用isMount变量区分mountupdate

在真实的Hooks中,组件mount时的hookupdate时的hook来源于不同的对象,这类对象在源码中被称为dispatcher

// mount时的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  // ...省略
};

// update时的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  // ...省略
};
复制代码

可见,mount时调用的hookupdate时调用的hook其实是两个不同的函数。

FunctionComponent render前,会根据FunctionComponent对应fiber的以下条件区分mountupdate

current === null || current.memoizedState === null
复制代码

并将不同情况对应的dispatcher赋值给全局变量ReactCurrentDispatchercurrent属性。

ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;  
复制代码

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

FunctionComponent render时,会从ReactCurrentDispatcher.current(即当前dispatcher)中寻找需要的hook

换言之,不同的调用栈上下文为ReactCurrentDispatcher.current赋值不同的dispatcher,则FunctionComponent render时调用的hook也是不同的函数。

除了这两个dispatcher,你可以在这里 (opens new window)看到其他dispatcher定义

#一个dispatcher使用场景

当错误的书写了嵌套形式的hook,如:

useEffect(() => {
  useState(0);
})
复制代码

此时ReactCurrentDispatcher.current已经指向ContextOnlyDispatcher,所以调用useState实际会调用throwInvalidHookError,直接抛出异常。

export const ContextOnlyDispatcher: Dispatcher = {
  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useImperativeHandle: throwInvalidHookError,
  useLayoutEffect: throwInvalidHookError,
  // ...省略
复制代码

你可以在这里 (opens new window)看到这段逻辑

#Hook的数据结构

接下来我们学习hook的数据结构。

const hook: Hook = {
  memoizedState: null,

  baseState: null,
  baseQueue: null,
  queue: null,

  next: null,
};
复制代码

你可以在这里 (opens new window)看到创建hook的逻辑

其中除memoizedState以外字段的意义与上一章介绍的updateQueue类似。

#memoizedState

注意

hookFunctionComponent fiber都存在memoizedState属性,不要混淆他们的概念。

  • fiber.memoizedStateFunctionComponent对应fiber保存的Hooks链表。
  • hook.memoizedStateHooks链表中保存的单一hook对应的数据。

不同类型hookmemoizedState保存不同类型数据,具体如下:

  • useState:对于const [state, updateState] = useState(initialState)memoizedState保存state的值
  • useReducer:对于const [state, dispatch] = useReducer(reducer, {});memoizedState保存state的值
  • useEffect:memoizedState保存包含useEffect回调函数依赖项等的链表数据结构effect,你可以在这里 (opens new window)看到effect的创建过程。effect链表同时会保存在fiber.updateQueue
  • useRef:对于useRef(1)memoizedState保存{current: 1}
  • useMemo:对于useMemo(callback, [depA])memoizedState保存[callback(), depA]
  • useCallback:对于useCallback(callback, [depA])memoizedState保存[callback, depA]。与useMemo的区别是,useCallback保存的是callback函数本身,而useMemo保存的是callback函数的执行结果

有些hook是没有memoizedState的,比如:

  • useContext

useState和useReducer

Redux的作者Dan加入React核心团队后的一大贡献就是“将Redux的理念带入React”。

这里面最显而易见的影响莫过于useStateuseReducer这两个Hook。本质来说,useState只是预置了reduceruseReducer

本节我们来学习useStateuseReducer的实现。

#流程概览

我们将这两个Hook的工作流程分为声明阶段调用阶段,对于:

function App() {
  const [state, dispatch] = useReducer(reducer, {a: 1});

  const [num, updateNum] = useState(0);
  
  return (
    <div>
      <button onClick={() => dispatch({type: 'a'})}>{state.a}</button>  
      <button onClick={() => updateNum(num => num + 1)}>{num}</button>  
    </div>
  )
}
复制代码

声明阶段App调用时,会依次执行useReduceruseState方法。

调用阶段即点击按钮后,dispatchupdateNum被调用时。

#声明阶段

FunctionComponent进入render阶段beginWork时,会调用renderWithHooks (opens new window)方法。

该方法内部会执行FunctionComponent对应函数(即fiber.type)。

你可以在这里 (opens new window)看到这段逻辑

对于这两个Hook,他们的源码如下:

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
function useReducer(reducer, initialArg, init) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialArg, init);
}
复制代码

正如上一节dispatcher所说,在不同场景下,同一个Hook会调用不同处理函数。

我们分别讲解mountupdate两个场景。

#mount时

mount时,useReducer会调用mountReducer (opens new window)useState会调用mountState (opens new window)

我们来简单对比这这两个方法:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 创建并返回当前的hook
  const hook = mountWorkInProgressHook();

  // ...赋值初始state

  // 创建queue
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });

  // ...创建dispatch
  return [hook.memoizedState, dispatch];
}

function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 创建并返回当前的hook
  const hook = mountWorkInProgressHook();

  // ...赋值初始state

  // 创建queue
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });

  // ...创建dispatch
  return [hook.memoizedState, dispatch];
}
复制代码

其中mountWorkInProgressHook方法会创建并返回对应hook,对应极简Hooks实现useState方法的isMount逻辑部分。

可以看到,mount时这两个Hook的唯一区别为queue参数的lastRenderedReducer字段。

queue的数据结构如下:

const queue = (hook.queue = {
  // 与极简实现中的同名字段意义相同,保存update对象
  pending: null,
  // 保存dispatchAction.bind()的值
  dispatch: null,
  // 上一次render时使用的reducer
  lastRenderedReducer: reducer,
  // 上一次render时的state
  lastRenderedState: (initialState: any),
});
复制代码

其中,useReducerlastRenderedReducer为传入的reducer参数。useStatelastRenderedReducerbasicStateReducer

basicStateReducer方法如下:

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}
复制代码

可见,useStatereducer参数为basicStateReduceruseReducer

mount时的整体运行逻辑与极简实现isMount逻辑类似,你可以对照着看。

#update时

如果说mount时这两者还有区别,那update时,useReduceruseState调用的则是同一个函数updateReducer (opens new window)

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 获取当前hook
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  
  queue.lastRenderedReducer = reducer;

  // ...同update与updateQueue类似的更新逻辑

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}
复制代码

整个流程可以概括为一句话:

找到对应的hook,根据update计算该hook的新state并返回。

mount时获取当前hook使用的是mountWorkInProgressHook,而update时使用的是updateWorkInProgressHook,这里的原因是:

  • mount时可以确定是调用ReactDOM.render或相关初始化API产生的更新,只会执行一次。
  • update可能是在事件回调或副作用中触发的更新或者是render阶段触发的更新,为了避免组件无限循环更新,后者需要区别对待。

举个render阶段触发的更新的例子:

function App() {
  const [num, updateNum] = useState(0);
  
  updateNum(num + 1);

  return (
    <button onClick={() => updateNum(num => num + 1)}>{num}</button>  
  )
}
复制代码

在这个例子中,App调用时,代表已经进入render阶段执行renderWithHooks

App内部,调用updateNum会触发一次更新。如果不对这种情况下触发的更新作出限制,那么这次更新会开启一次新的render阶段,最终会无限循环更新。

基于这个原因,React用一个标记变量didScheduleRenderPhaseUpdate判断是否是render阶段触发的更新。

updateWorkInProgressHook方法也会区分这两种情况来获取对应hook

获取对应hook,接下来会根据hook中保存的state计算新的state,这个步骤同Update一节一致。

#调用阶段

调用阶段会执行dispatchAction (opens new window),此时该FunctionComponent对应的fiber以及hook.queue已经通过调用bind方法预先作为参数传入。

function dispatchAction(fiber, queue, action) {

  // ...创建update
  var update = {
    eventTime: eventTime,
    lane: lane,
    suspenseConfig: suspenseConfig,
    action: action,
    eagerReducer: null,
    eagerState: null,
    next: null
  }; 

  // ...将update加入queue.pending
  
  var alternate = fiber.alternate;

  if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
    // render阶段触发的更新
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  } else {
    if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
      // ...fiber的updateQueue为空,优化路径
    }

    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
}
复制代码

整个过程可以概括为:

创建update,将update加入queue.pending中,并开启调度。

这里值得注意的是if...else...逻辑,其中:

if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1)
复制代码

currentlyRenderingFiberworkInProgressworkInProgress存在代表当前处于render阶段

触发更新时通过bind预先保存的fiberworkInProgress全等,代表本次更新发生于FunctionComponent对应fiberrender阶段

所以这是一个render阶段触发的更新,需要标记变量didScheduleRenderPhaseUpdate,后续单独处理。

再来关注:

if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes))
复制代码

fiber.lanes保存fiber上存在的update优先级

fiber.lanes === NoLanes意味着fiber上不存在update

我们已经知道,通过update计算state发生在声明阶段,这是因为该hook上可能存在多个不同优先级update,最终state的值由多个update共同决定。

但是当fiber上不存在update,则调用阶段创建的update为该hook上第一个update,在声明阶段计算state时也只依赖于该update,完全不需要进入声明阶段再计算state

这样做的好处是:如果计算出的state与该hook之前保存的state一致,那么完全不需要开启一次调度。即使计算出的state与该hook之前保存的state不一致,在声明阶段也可以直接使用调用阶段已经计算出的state

你可以在这里 (opens new window)看到这段提前计算state的逻辑

#小Tip

我们通常认为,useReducer(reducer, initialState)的传参为初始化参数,在以后的调用中都不可变。

但是在updateReducer方法中,可以看到lastRenderedReducer在每次调用时都会重新赋值。

function updateReducer(reducer, initialArg, init) {
  // ...

  queue.lastRenderedReducer = reducer;

  // ...
复制代码

也就是说,reducer参数是随时可变的。

useEffect

架构篇commit阶段流程概览我们讲解了useEffect的工作流程。

其中我们谈到

flushPassiveEffects方法内部会从全局变量rootWithPendingPassiveEffects获取effectList

本节我们深入flushPassiveEffects方法内部探索useEffect的工作原理。

#flushPassiveEffectsImpl

flushPassiveEffects内部会设置优先级,并执行flushPassiveEffectsImpl

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

flushPassiveEffectsImpl主要做三件事:

  • 调用该useEffect在上一次render时的销毁函数
  • 调用该useEffect在本次render时的回调函数
  • 如果存在同步任务,不需要等待下次事件循环宏任务,提前执行他

本节我们关注前两步。

v16中第一步是同步执行的,在官方博客 (opens new window)中提到:

副作用清理函数(如果存在)在 React 16 中同步运行。我们发现,对于大型应用程序来说,这不是理想选择,因为同步会减缓屏幕的过渡(例如,切换标签)。

基于这个原因,在v17.0.0中,useEffect的两个阶段会在页面渲染后(layout阶段后)异步执行。

事实上,从代码中看,v16.13.1中已经是异步执行了

接下来我们详细讲解这两个步骤。

#阶段一:销毁函数的执行

useEffect的执行需要保证所有组件useEffect销毁函数必须都执行完后才能执行任意一个组件的useEffect回调函数

这是因为多个组件间可能共用同一个ref

如果不是按照“全部销毁”再“全部执行”的顺序,那么在某个组件useEffect销毁函数中修改的ref.current可能影响另一个组件useEffect回调函数中的同一个refcurrent属性。

useLayoutEffect中也有同样的问题,所以他们都遵循“全部销毁”再“全部执行”的顺序。

在阶段一,会遍历并执行所有useEffect销毁函数

// pendingPassiveHookEffectsUnmount中保存了所有需要执行销毁的useEffect
const unmountEffects = pendingPassiveHookEffectsUnmount;
  pendingPassiveHookEffectsUnmount = [];
  for (let i = 0; i < unmountEffects.length; i += 2) {
    const effect = ((unmountEffects[i]: any): HookEffect);
    const fiber = ((unmountEffects[i + 1]: any): Fiber);
    const destroy = effect.destroy;
    effect.destroy = undefined;

    if (typeof destroy === 'function') {
      // 销毁函数存在则执行
      try {
        destroy();
      } catch (error) {
        captureCommitPhaseError(fiber, error);
      }
    }
  }
复制代码

其中pendingPassiveHookEffectsUnmount数组的索引i保存需要销毁的effecti+1保存该effect对应的fiber

pendingPassiveHookEffectsUnmount数组内push数据的操作发生在layout阶段 commitLayoutEffectOnFiber方法内部的schedulePassiveEffects方法中。

commitLayoutEffectOnFiber方法我们在Layout阶段已经介绍

function schedulePassiveEffects(finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      const {next, tag} = effect;
      if (
        (tag & HookPassive) !== NoHookEffect &&
        (tag & HookHasEffect) !== NoHookEffect
      ) {
        // 向`pendingPassiveHookEffectsUnmount`数组内`push`要销毁的effect
        enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
        // 向`pendingPassiveHookEffectsMount`数组内`push`要执行回调的effect
        enqueuePendingPassiveHookEffectMount(finishedWork, effect);
      }
      effect = next;
    } while (effect !== firstEffect);
  }
}
复制代码

#阶段二:回调函数的执行

与阶段一类似,同样遍历数组,执行对应effect回调函数

其中向pendingPassiveHookEffectsMountpush数据的操作同样发生在schedulePassiveEffects中。

// pendingPassiveHookEffectsMount中保存了所有需要执行回调的useEffect
const mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (let i = 0; i < mountEffects.length; i += 2) {
  const effect = ((mountEffects[i]: any): HookEffect);
  const fiber = ((mountEffects[i + 1]: any): Fiber);
  
  try {
    const create = effect.create;
   effect.destroy = create();
  } catch (error) {
    captureCommitPhaseError(fiber, error);
  }
}
复制代码

useRef

refreference(引用)的缩写。在React中,我们习惯用ref保存DOM

事实上,任何需要被"引用"的数据都可以保存在ref中,useRef的出现将这种思想进一步发扬光大。

Hooks数据结构一节我们讲到:

对于useRef(1)memoizedState保存{current: 1}

本节我们会介绍useRef的实现,以及ref的工作流程。

由于string类型的ref已不推荐使用,所以本节针对function | {current: any}类型的ref

#useRef

与其他Hook一样,对于mountupdateuseRef对应两个不同dispatcher

function mountRef<T>(initialValue: T): {|current: T|} {
  // 获取当前useRef hook
  const hook = mountWorkInProgressHook();
  // 创建ref
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

function updateRef<T>(initialValue: T): {|current: T|} {
  // 获取当前useRef hook
  const hook = updateWorkInProgressHook();
  // 返回保存的数据
  return hook.memoizedState;
}
复制代码

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

可见,useRef仅仅是返回一个包含current属性的对象。

为了验证这个观点,我们再看下React.createRef方法的实现:

export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  return refObject;
}
复制代码

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

了解了ref的数据结构后,我们再来看看ref的工作流程。

#ref的工作流程

React中,HostComponentClassComponentForwardRef可以赋值ref属性。

// HostComponent
<div ref={domRef}></div>
// ClassComponent / ForwardRef
<App ref={cpnRef} />
复制代码

其中,ForwardRef只是将ref作为第二个参数传递下去,不会进入ref的工作流程。

所以接下来讨论ref的工作流程时会排除ForwardRef

// 对于ForwardRef,secondArg为传递下去的ref
let children = Component(props, secondArg);
复制代码

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

我们知道HostComponentcommit阶段mutation阶段执行DOM操作。

所以,对应ref的更新也是发生在mutation阶段

再进一步,mutation阶段执行DOM操作的依据为effectTag

所以,对于HostComponentClassComponent如果包含ref操作,那么也会赋值相应的effectTag

// ...
export const Placement = /*                    */ 0b0000000000000010;
export const Update = /*                       */ 0b0000000000000100;
export const Deletion = /*                     */ 0b0000000000001000;
export const Ref = /*                          */ 0b0000000010000000;
// ...
复制代码

你可以在ReactSideEffectTags文件 (opens new window)中看到ref对应的effectTag

所以,ref的工作流程可以分为两部分:

  • render阶段为含有ref属性的fiber添加Ref effectTag
  • commit阶段为包含Ref effectTagfiber执行对应操作

#render阶段

render阶段beginWorkcompleteWork中有个同名方法markRef用于为含有ref属性的fiber增加Ref effectTag

// beginWork的markRef
function markRef(current: Fiber | null, workInProgress: Fiber) {
  const ref = workInProgress.ref;
  if (
    (current === null && ref !== null) ||
    (current !== null && current.ref !== ref)
  ) {
    // Schedule a Ref effect
    workInProgress.effectTag |= Ref;
  }
}
// completeWork的markRef
function markRef(workInProgress: Fiber) {
  workInProgress.effectTag |= Ref;
}
复制代码

你可以在这里 (opens new window)看到beginWorkmarkRef这里 (opens new window)看到completeWorkmarkRef

beginWork中,如下两处调用了markRef

注意ClassComponent即使shouldComponentUpdatefalse该组件也会调用markRef

completeWork中,如下两处调用了markRef

ScopeComponent是一种用于管理focus的测试特性,详见PR(opens new window)

总结下组件对应fiber被赋值Ref effectTag需要满足的条件:

  • fiber类型为HostComponentClassComponentScopeComponent(这种情况我们不讨论)
  • 对于mountworkInProgress.ref !== null,即存在ref属性
  • 对于updatecurrent.ref !== workInProgress.ref,即ref属性改变

#commit阶段

commit阶段mutation阶段中,对于ref属性改变的情况,需要先移除之前的ref

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  while (nextEffect !== null) {

    const effectTag = nextEffect.effectTag;
    // ...

    if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        // 移除之前的ref
        commitDetachRef(current);
      }
    }
    // ...
  }
  // ...
复制代码

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

function commitDetachRef(current: Fiber) {
  const currentRef = current.ref;
  if (currentRef !== null) {
    if (typeof currentRef === 'function') {
      // function类型ref,调用他,传参为null
      currentRef(null);
    } else {
      // 对象类型refcurrent赋值为null
      currentRef.current = null;
    }
  }
}
复制代码

接下来,在mutation阶段,对于Deletion effectTagfiber(对应需要删除的DOM节点),需要递归他的子树,对子孙fiberref执行类似commitDetachRef的操作。

mutation阶段一节我们讲到

对于Deletion effectTagfiber,会执行commitDeletion

commitDeletion——unmountHostComponents——commitUnmount——ClassComponent | HostComponent类型case中调用的safelyDetachRef方法负责执行类似commitDetachRef的操作。

function safelyDetachRef(current: Fiber) {
  const ref = current.ref;
  if (ref !== null) {
    if (typeof ref === 'function') {
      try {
        ref(null);
      } catch (refError) {
        captureCommitPhaseError(current, refError);
      }
    } else {
      ref.current = null;
    }
  }
}
复制代码

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

接下来进入ref的赋值阶段。我们在Layout阶段一节讲到

commitLayoutEffect会执行commitAttachRef(赋值ref

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    // 获取ref属性对应的Component实例
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }

    // 赋值ref
    if (typeof ref === 'function') {
      ref(instanceToUse);
    } else {
      ref.current = instanceToUse;
    }
  }
}
复制代码

至此,ref的工作流程完毕。

#总结

本节我们学习了ref的工作流程。

  • 对于FunctionComponentuseRef负责创建并返回对应的ref
  • 对于赋值了ref属性的HostComponentClassComponent,会在render阶段经历赋值Ref effectTag,在commit阶段执行对应ref操作。

useMemo和useCallback

在了解其他hook的实现后,理解useMemouseCallback的实现非常容易。

本节我们以mountupdate两种情况分别讨论这两个hook

#mount

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 创建并返回当前hook
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 计算value
  const nextValue = nextCreate();
  // 将value与deps保存在hook.memoizedState
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 创建并返回当前hook
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 将value与deps保存在hook.memoizedState
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
复制代码

可以看到,与mountCallback这两个唯一的区别是

  • mountMemo会将回调函数(nextCreate)的执行结果作为value保存
  • mountCallback会将回调函数作为value保存

#update

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 返回当前hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 判断update前后value是否变化
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 未变化
        return prevState[0];
      }
    }
  }
  // 变化,重新计算value
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 返回当前hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 判断update前后value是否变化
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 未变化
        return prevState[0];
      }
    }
  }

  // 变化,将新的callback作为value
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
复制代码

可见,对于update,这两个hook的唯一区别也是是回调函数本身还是回调函数的执行结果作为value

第八章 Concurrent Mode

概览

在了解其他hook的实现后,理解useMemouseCallback的实现非常容易。

本节我们以mountupdate两种情况分别讨论这两个hook

#mount

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 创建并返回当前hook
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 计算value
  const nextValue = nextCreate();
  // 将value与deps保存在hook.memoizedState
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 创建并返回当前hook
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 将value与deps保存在hook.memoizedState
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
复制代码

可以看到,与mountCallback这两个唯一的区别是

  • mountMemo会将回调函数(nextCreate)的执行结果作为value保存
  • mountCallback会将回调函数作为value保存

#update

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 返回当前hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 判断update前后value是否变化
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 未变化
        return prevState[0];
      }
    }
  }
  // 变化,重新计算value
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 返回当前hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 判断update前后value是否变化
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 未变化
        return prevState[0];
      }
    }
  }

  // 变化,将新的callback作为value
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
复制代码

可见,对于update,这两个hook的唯一区别也是是回调函数本身还是回调函数的执行结果作为value

Scheduler的原理和实现

新的React架构一节我们介绍了Scheduler,他包含两个功能:

  1. 时间切片
  2. 优先级调度

本节我们学习这个两个功能是如何在Scheduler中实现的。

#时间切片原理

时间切片的本质是模拟实现requestIdleCallback (opens new window)

除去“浏览器重排/重绘”,下图是浏览器一帧中可以用于执行JS的时机。

一个task(宏任务) -- 队列中全部job(微任务) -- requestAnimationFrame -- 浏览器重排/重绘 -- requestIdleCallback
复制代码

requestIdleCallback是在“浏览器重排/重绘”后如果当前帧还有空余时间时被调用的。

浏览器并没有提供其他API能够在同样的时机(浏览器重排/重绘后)调用以模拟其实现。

唯一能精准控制调用时机的APIrequestAnimationFrame,他能让我们在“浏览器重排/重绘”之前执行JS

这也是为什么我们通常用这个API实现JS动画 —— 这是浏览器渲染前的最后时机,所以动画能快速被渲染。

所以,退而求其次,Scheduler时间切片功能是通过task(宏任务)实现的。

最常见的task当属setTimeout了。但是有个tasksetTimeout执行时机更靠前,那就是MessageChannel (opens new window)

所以Scheduler将需要被执行的回调函数作为MessageChannel的回调执行。如果当前宿主环境不支持MessageChannel,则使用setTimeout

你可以在这里 (opens new window)看到MessageChannel的实现。这里 (opens new window)看到setTimeout的实现

Reactrender阶段,开启Concurrent Mode时,每次遍历前,都会通过Scheduler提供的shouldYield方法判断是否需要中断遍历,使浏览器有时间渲染:

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
复制代码

是否中断的依据,最重要的一点便是每个任务的剩余时间是否用完。

Schdeduler中,为任务分配的初始剩余时间为5ms

你可以从这里 (opens new window)看到初始剩余时间的定义

随着应用运行,会通过fps动态调整分配给任务的可执行时间。

你可以从这里 (opens new window)看到动态分配任务时间

这也解释了为什么设计理念一节启用Concurrent Mode后每个任务的执行时间大体都是多于5ms的一小段时间 —— 每个时间切片被设定为5ms,任务本身再执行一小段时间,所以整体时间是多于5ms的时间

长任务

那么当shouldYieldtrue,以至于performUnitOfWork被中断后是如何重新启动的呢?我们会在介绍完"优先级调度"后解答。

#优先级调度

首先我们来了解优先级的来源。需要明确的一点是,Scheduler是独立于React的包,所以他的优先级也是独立于React优先级的。

Scheduler对外暴露了一个方法unstable_runWithPriority (opens new window)

这个方法接受一个优先级与一个回调函数,在回调函数内部调用获取优先级的方法都会取得第一个参数对应的优先级

function unstable_runWithPriority(priorityLevel, eventHandler) {
  switch (priorityLevel) {
    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
  }

  var previousPriorityLevel = currentPriorityLevel;
  currentPriorityLevel = priorityLevel;

  try {
    return eventHandler();
  } finally {
    currentPriorityLevel = previousPriorityLevel;
  }
}
复制代码

可以看到,Scheduler内部存在5种优先级。

React内部凡是涉及到优先级调度的地方,都会使用unstable_runWithPriority

比如,我们知道commit阶段是同步执行的。可以看到,commit阶段的起点commitRoot方法的优先级为ImmediateSchedulerPriority

ImmediateSchedulerPriorityImmediatePriority的别名,为最高优先级,会立即执行。

function commitRoot(root) {
  const renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority(
    ImmediateSchedulerPriority,
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
  return null;
}
复制代码

#优先级的意义

Scheduler对外暴露最重要的方法便是unstable_scheduleCallback (opens new window)。该方法用于以某个优先级注册回调函数。

比如在React中,之前讲过在commit阶段的beforeMutation阶段会调度useEffect的回调:

if (!rootDoesHavePassiveEffects) {
  rootDoesHavePassiveEffects = true;
  scheduleCallback(NormalSchedulerPriority, () => {
    flushPassiveEffects();
    return null;
  });
}
复制代码

这里的回调便是通过scheduleCallback调度的,优先级为NormalSchedulerPriority,即NormalPriority

不同优先级意味着什么?不同优先级意味着不同时长的任务过期时间:

var timeout;
switch (priorityLevel) {
  case ImmediatePriority:
    timeout = IMMEDIATE_PRIORITY_TIMEOUT;
    break;
  case UserBlockingPriority:
    timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
    break;
  case IdlePriority:
    timeout = IDLE_PRIORITY_TIMEOUT;
    break;
  case LowPriority:
    timeout = LOW_PRIORITY_TIMEOUT;
    break;
  case NormalPriority:
  default:
    timeout = NORMAL_PRIORITY_TIMEOUT;
    break;
}

var expirationTime = startTime + timeout;
复制代码

其中:

// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
复制代码

可以看到,如果一个任务的优先级ImmediatePriority,对应IMMEDIATE_PRIORITY_TIMEOUT-1,那么

var expirationTime = startTime - 1;
复制代码

则该任务的过期时间比当前时间还短,表示他已经过期了,需要立即被执行。

#不同优先级任务的排序

我们已经知道优先级意味着任务的过期时间。设想一个大型React项目,在某一刻,存在很多不同优先级任务,对应不同的过期时间。

同时,又因为任务可以被延迟,所以我们可以将这些任务按是否被延迟分为:

  • 已就绪任务
  • 未就绪任务
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      // 任务被延迟
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }
复制代码

所以,Scheduler存在两个队列:

  • timerQueue:保存未就绪任务
  • taskQueue:保存已就绪任务

每当有新的未就绪的任务被注册,我们将其插入timerQueue并根据开始时间重新排列timerQueue中任务的顺序。

timerQueue中有任务就绪,即startTime <= currentTime,我们将其取出并加入taskQueue

取出taskQueue中最早过期的任务并执行他。

为了能在O(1)复杂度找到两个队列中时间最早的那个任务,Scheduler使用小顶堆 (opens new window)实现了优先级队列

你可以在这里 (opens new window)看到优先级队列的实现

至此,我们了解了Scheduler的实现。现在可以回答介绍时间切片时提到的问题:

那么当shouldYield为true,以至于performUnitOfWork被中断后是如何重新启动的呢?

在“取出taskQueue中最早过期的任务并执行他”这一步中有如下关键步骤:

const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
  // continuationCallback是函数
  currentTask.callback = continuationCallback;
  markTaskYield(currentTask, currentTime);
} else {
  if (enableProfiling) {
    markTaskCompleted(currentTask, currentTime);
    currentTask.isQueued = false;
  }
  if (currentTask === peek(taskQueue)) {
    // 将当前任务清除
    pop(taskQueue);
  }
}
advanceTimers(currentTime);
复制代码

当注册的回调函数执行后的返回值continuationCallbackfunction,会将continuationCallback作为当前任务的回调函数。

如果返回值不是function,则将当前被执行的任务清除出taskQueue

render阶段被调度的函数为performConcurrentWorkOnRoot,在该函数末尾有这样一段代码:

if (root.callbackNode === originalCallbackNode) {
  // The task node scheduled for this root is the same one that's
  // currently executed. Need to return a continuation.
  return performConcurrentWorkOnRoot.bind(null, root);
}
复制代码

可以看到,在满足一定条件时,该函数会将自己作为返回值。

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

#总结

刚才我们讲到,SchedulerReact是两套优先级机制。那么React中的优先级是如何运转的?我们会在下一节介绍。

lane模型

上一节我们提到SchedulerReact是两套优先级机制。在React中,存在多种使用不同优先级的情况,比如:

注:以下例子皆为Concurrent Mode开启情况

  • 过期任务或者同步任务使用同步优先级
  • 用户交互产生的更新(比如点击事件)使用高优先级
  • 网络请求产生的更新使用一般优先级
  • Suspense使用低优先级

React需要设计一套满足如下需要的优先级机制:

  • 可以表示优先级的不同
  • 可能同时存在几个同优先级更新,所以还得能表示的概念
  • 方便进行优先级相关计算

为了满足如上需求,React设计了lane模型。接下来我们来看lane模型如何满足以上3个条件。

#表示优先级的不同

想象你身处赛车场。

30sec

不同的赛车疾驰在不同的赛道。内圈的赛道总长度更短,外圈更长。某几个临近的赛道的长度可以看作差不多长。

lane模型借鉴了同样的概念,使用31位的二进制表示31条赛道,位数越小的赛道优先级越高,某些相邻的赛道拥有相同优先级

如下:

export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = /*                 */ 0b0000000000000000000000000000010;

export const InputDiscreteHydrationLane: Lane = /*      */ 0b0000000000000000000000000000100;
const InputDiscreteLanes: Lanes = /*                    */ 0b0000000000000000000000000011000;

const InputContinuousHydrationLane: Lane = /*           */ 0b0000000000000000000000000100000;
const InputContinuousLanes: Lanes = /*                  */ 0b0000000000000000000000011000000;

export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000100000000;
export const DefaultLanes: Lanes = /*                   */ 0b0000000000000000000111000000000;

const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000001000000000000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111110000000000000;

const RetryLanes: Lanes = /*                            */ 0b0000011110000000000000000000000;

export const SomeRetryLane: Lanes = /*                  */ 0b0000010000000000000000000000000;

export const SelectiveHydrationLane: Lane = /*          */ 0b0000100000000000000000000000000;

const NonIdleLanes = /*                                 */ 0b0000111111111111111111111111111;

export const IdleHydrationLane: Lane = /*               */ 0b0001000000000000000000000000000;
const IdleLanes: Lanes = /*                             */ 0b0110000000000000000000000000000;

export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;
复制代码

你可以在这里 (opens new window)看到lane的定义

其中,同步优先级占用的赛道为第一位:

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
复制代码

SyncLane往下一直到SelectiveHydrationLane,赛道的优先级逐步降低。

#表示“批”的概念

可以看到其中有几个变量占用了几条赛道,比如:

const InputDiscreteLanes: Lanes = /*                    */ 0b0000000000000000000000000011000;
export const DefaultLanes: Lanes = /*                   */ 0b0000000000000000000111000000000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111110000000000000;
复制代码

这就是的概念,被称作lanes(区别于优先级lane)。

其中InputDiscreteLanes是“用户交互”触发更新会拥有的优先级范围。

DefaultLanes是“请求数据返回后触发更新”拥有的优先级范围。

TransitionLanesSuspenseuseTransitionuseDeferredValue拥有的优先级范围。

这其中有个细节,越低优先级lanes占用的位越多。比如InputDiscreteLanes占了2个位,TransitionLanes占了9个位。

原因在于:越低优先级更新越容易被打断,导致积压下来,所以需要更多的位。相反,最高优的同步更新的SyncLane不需要多余的lanes

#方便进行优先级相关计算

既然lane对应了二进制的位,那么优先级相关计算其实就是位运算。

比如:

计算ab两个lane是否存在交集,只需要判断ab按位与的结果是否为0

export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
  return (a & b) !== NoLanes;
}
复制代码

计算b这个lanes是否是a对应的lanes的子集,只需要判断ab按位与的结果是否为b

export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {
  return (set & subset) === subset;
}
复制代码

将两个lanelanes的位合并只需要执行按位或操作:

export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a | b;
}
复制代码

set对应lanes中移除subset对应lane(或lanes),只需要对subsetlane(或lanes)执行按位非,结果再对set执行按位与。

export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
  return set & ~subset;
}
复制代码

更多位运算参考MDN(opens new window)

#总结

这就是React的优先级模型lane模型。

至此,我们已经了解Fiber架构、更新优先级Scheduler的实现、lane模型。从下一节开始,我们会逐步讲解Concurrent Mode的各种应用。

异步可中断更新与饥饿问题

batchedUpdates的实现

高优先级更新如何插队

Suspense的实现(ke.segmentfault.com/course/1650…)

施工中...

分类:
阅读
标签:
收藏成功!
已添加到「」, 点击更改