1. 背景
在我们初学Hooks的时候,相信很多人都遇到过这样的报错:Hooks can only be called inside of the body of a function component. 当遇到这个报错后,通常经过查询 📖 我们会知道 Hooks 只能在函数组件的主体内部调用这一规则,但如果没有对 Hooks 进行一个完整的系统性学习,在项目代码中可能仍然会错误的使用 Hooks。举个简单的例子🌰:
在这两段代码片段内都在条件语句中使用了 Hooks,因为这样的使用不会报错,所以这往往并不会引起我们的注意,但实际上这并不是正确规范的使用,Hooks 的使用规则除了是只能在函数组件的主体内部调用,并且是只能在函数组件主体的顶层调用它们,React 官方文档里有对此规则进行简单阐述:
仅在顶层调用 Hooks,不要在循环、条件或嵌套函数中调用 Hooks。相反,在任何提前返回之前,始终在React 函数的顶层使用 Hooks。通过遵循此规则,您可以确保每次渲染组件时都以相同的顺序调用 Hook。这就是允许 React 在多个 useState 和 useEffect 调用之间正确保留 Hooks 状态的原因。
我们的学习往往是从接纳一些定理规则开始,然后再到深入了解这些定理规则的源由。对于 Hooks 而言,一开始可能我们也只是谨记着 Hooks 的使用规则,但随着不断的使用,心里对于 Hooks 的疑惑 🤔 肯定会越来越多,比如 为什么只能在函数顶层使用 Hooks 而不要在循环、条件或嵌套函数中调用 Hook? 或者再深入一点的思考,我们知道类组件因为自身的特点可以将私有状态持久化的挂载到类实例上,每时每刻保存的都是最新的值,而函数式组件由于本质就是一个函数,并且每次渲染都会重新执行,那 对于函数式组件来说 React 是如何在每次重新渲染之后都能返回最新的状态?这些状态究竟存放在哪?
我们猜测 React 必定拥有某种机制去记住每一次的更新操作,最终得出最新的值并将其返回。对于这些问题,如果单从官网学习或者检索的零散解释可能依然会让人云里雾里,摸不着头脑,这时候我们其实可以从 Hooks 源码来进行学习,通过剖析 React-hooks 运行机制和内部原理来解答疑惑。
💡 为什么要阅读源码?阅读源码并不只是让我们能够更加深入地理解一个框架的运作原理,更能让我们在一些实现方案上学习到一些更优的方法,提炼我们的设计思维。
2. 源码解析
⚠️ 注意:为了让我们能够快速 🔜 地看懂 React 源码中与 Hooks 相关的核心逻辑,本文中贴出代码片段,都去除了与主流程逻辑无关的容错代码以及和 __DEV__ 相关的部分代码!
2.1 入口
我们从引入 hooks 开始,以 useState 为例子,通常我们会以这种方式来引入 useState:
import { useState } from 'react'
被引入的这个 useState 方法是什么样子的呢?其实这个方法就在源码 packages/react/src/ReactHook.js 中:
// packages/react/src/ReactHook.js
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
当我们第一眼 👀 看到源码时,咋一看怎么有些复杂?这其实是源码中函数的参数以及返回值类型带来的一种错觉,我们可以先跳过函数的参数以及返回值类型,直接看逻辑。
该函数内部首先调用了一个 resolveDispatcher 函数并将调用结果赋值给了 dispatcher 变量,接着就通过 return 的这个 dispatcher 调用了 useState 函数。所以其实本质上就是通过 resolveDispatcher() 这个方法生成一个 dispatcher 调度器,然后通过该调度器去调取实际执行用户所希望的 hook 的真实逻辑。
那我们当然就好奇了,这个 resolveDispatcher 函数中的 dispatcher 到底是什么?于是继续往下挖源码:
// packages/react/src/ReactHook.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return ((dispatcher: any): Dispatcher);
}
从这里可以看出,我们使用的 useState 最终调用的其实是 ReactCurrentDispatcher.current.useSate。也就是说,Hooks API 其实是挂载在 ReactCurrentDispatcher.current 上面的,那我们再继续去看一下 ReactCurrentDispatcher 是什么:
// packages/react/src/ReactCurrentDispatcher.js
import type { Dispatcher } from 'react-reconciler/src/ReactInternalTypes';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
在这里,我们看到一个比较特别的语法:import type,它表示的是从另一个文件模块中导入一个数据类型,也就是 /packages/react-reconciler/src/ReactInternalTypes.js 文件的 Dispatcher 类型,进入到该文件后,会发现里面只暴露出了 Dispatcher 的类型定义,hooks 的详细源码以及 ReactCurrentDispatcher 的具体内容我们并没有找到在哪里,并且比较尴尬 😅 的是看到这里就没有下文了,这意味着我们的线索从这里就断掉了。所以我们只能另寻出路,换个思路从 Hooks 的上一层进行分析。
2.2 源码定位
前面有提及到,我们的 Hooks 都是在函数式组件中使用的,所以我们可以从 React 函数式组件的执行过程去入手。最简单的办法就是查看调用栈,所以我们准备一个最简单的 demo,在游览器开发者模式中进行 breakpoint断点调试,看看在 mount 阶段 React 执行了什么。下图 ⬇️ 红色部分就是 Call Stack 函数调用栈信息,表示的是React 在初始化阶段执行的函数,从中可以清晰的看出,在 mount 阶段最先执行的是 renderWithHooks,并且整个调用栈中似乎只有 renderWithHooks 看起来和 Hooks 有关联,因此我们可以大胆地进行猜测,React Hooks 的渲染核心入口就是 renderWithHooks。
如果你对 React 更新流程这一块的源码比较熟悉的话, 你应该知道 Fiber 架构下组件的创建与更新,其本质上就是去构建一个由多个 Fiber 节点相连组成的 Fiber 树的过程。而创建和更新 Fiber 节点树,React 又将其分为了Render 和 Commit 两个阶段去完成。其中,Render 阶段主要做的一个事情就是生成一个用于更新的 Fiber 节点树,整个过程会从根结点开始向下深度优先遍历,对遍历到的每一个 Fiber 节点进行 beginWork 方法的调用,该方法会根据传入的 Fiber 节点创建子 Fiber 节点,并将这两个 Fiber 节点连接起来。也就是说,Render 过程中的调度是从 beginWork() 开始执行的,因此我们可以去看一看 beginWork 的源码。
2.2.1 beginWork
beginWork 是在 packages/react-reconciler/src/ReactFiberBeginWork.js 文件中定义的 ,来到 beginWork 的源码后我们可以发现,对于 Function Component 函数式组件,其走以下逻辑加载或更新组件:
// packages/react-reconciler/src/ReactFiberBeginWork.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
switch (workInProgress.tag) {
// 其他类型组件代码省略...
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
// 组件更新
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
}
}
2.2.2 updateFunctionComponent
由于 beginWork 函数的工作是传入当前 Fiber 节点,创建子 Fiber 节点,因此在 updateFunctionComponent 中,我们主要关心的是创建子 Fiber 节点的过程:
// packages/react-reconciler/src/ReactFiberBeginWork.js
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderExpirationTime,
);
2.2.3 renderWithHooks
函数式组件作为一个函数,它的渲染其实就是函数调用。从上边我们可看出,renderWithHooks() 是调用函数式组件的主要函数,而 Hooks 又都是在函数式组件中使用的,由此证实了我们之前的猜想,React Hooks 的渲染核心入口是 renderWithHooks。
我们来到 packages/react-reconciler/src/ReactFiberHooks.new.js 来着重看看 renderWithHooks 及其之后的逻辑,其他的渲染流程我们暂时不用关心,因此当前我们不需要去明白renderWithHooks 中每一行代码的意思,只需要找到和 Hooks 相关的部分代码进行分析即可:
// packages/react-reconciler/src/ReactFiberHooks.new.js
/**
* @param {*} current 当前屏幕上显示内容对应的Fiber树
* @param {*} workInProgress 正在内存中构建的Fiber树,它反映了要刷新到屏幕的未来状态
* @param {*} Component 当前组件
*/
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
// 在执行组件方法之前,需要进行进行一些清空操作,然后在后续重新创建
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// 根据是mount阶段还是update阶段,对ReactCurrentDispatcher.current进行不同的赋值,获取不同的hooks函数集合
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 调用函数组件,此时组件里的hooks会被依次执行
let children = Component(props, secondArg);
// 如果在render阶段发生了更新,会直接re-render重新执行,直到不产生新的更新为止。
if (didScheduleRenderPhaseUpdateDuringThisPass) {
let numberOfReRenders: number = 0;
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
if (numberOfReRenders >= RE_RENDER_LIMIT) {
throw new Error(
'Too many re-renders. React limits the number of renders to prevent ' + 'an infinite loop.',
);
}
numberOfReRenders += 1;
currentHook = null; // currentHook在组件更新阶段对应是老的hook
workInProgressHook = null; // workInProgressHook存储的是当前最新的hook
workInProgress.updateQueue = null;
ReactCurrentDispatcher.current = HooksDispatcherOnRerender;
children = Component(props, secondArg);
} while (didScheduleRenderPhaseUpdateDuringThisPass);
}
// 函数组件的render已结束,关闭hooks调用接口
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
// 当前fiber已执行结束,重置这些全局变量
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
if (didRenderTooFewHooks) {
throw new Error(
'Rendered fewer hooks than expected. This may be caused by an accidental ' + 'early return statement.',
);
}
return children;
}
我们在 renderWithHooks 中看到了 ReactCurrentDispatcher.current 的赋值,到这里,终于和前面讲到的 ReactCurrentDispatcher 联系到了一起。
current 参数表示当前屏幕上显示内容对应的 Fiber 树,在 mount 首次渲染阶段,是不存在 current Fiber 树的,即在 mount 阶段时 current === null。所以我们可以通过 current === null? 来区分组件是处于 mount 阶段还是 update 阶段。组件如果处于 mount 阶段,那么 ReactCurrentDispatcher.current 将被赋值成HooksDispatcherOnMount,否则将被赋值为 HooksDispatcherOnUpdate。这里启示着我们下一步需要从 HooksDispatcherOnMount 和 HooksDispatcherOnUpdate 入手研究 Hooks。
2.2.4 HooksDispatcherOnMount & HooksDispatcherOnUpdate
// packages/react-reconciler/src/ReactFiberHooks.new.js
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
unstable_isNewReconciler: enableNewReconciler,
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useId: updateId,
unstable_isNewReconciler: enableNewReconciler,
};
我们可以看到,每一个 hook 都有两个相关的函数:mountXXX 以及 updateXXX,这两个函数分别是 Hook 在 mount 首次渲染阶段和 update 组件更新阶段的逻辑。
也就是说,同一个 Hook 在 mount 和 update 阶段所做的事情是不一样的,在挂载和更新组件时,调用的 Hook其实是两个不同的函数。举个栗子 🌰,在首次加载函数组件时,我们在组件中调用的 useSate 实际上调用的是ReactCurrentDispatcher.current.useState = HooksDispatcherOnMount.useState = mountState,而在后续的更新渲染阶段实际上调用的是ReactCurrentDispatcher.current.useState = HooksDispatcherOnUpdate.useState = updateState,其他的 hooks 也是同理。
2.3 组件挂载
2.3.1 mountState
// packages/react-reconciler/src/ReactFiberHooks.new.js
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 创建一个新的 Hook,将这个新创建的 Hook 添加到 Hook 链表中并返回该 Hook 链表
const hook = mountWorkInProgressHook();
// 初始值如果是函数,就执行函数拿到初始值
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
// 把初始值赋值给 hook.baseState 和 hook.memoizedState
hook.memoizedState = hook.baseState = initialState;
// 存放更新操作
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null, // 存放 update 对象
interleaved: null, // 放 hooks 更新函数
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer, // 它是一个函数,用于得到最新的 state
lastRenderedState: (initialState: any), // 最后一次得到的 state
};
hook.queue = queue;
// dispatchAction 是负责更新的函数,就是代表下面的setState函数
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
// 将 state 值和 state 更新函数以数组的形式返回
return [hook.memoizedState, dispatch];
}
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
2.3.2 mountWorkInProgressHook
// packages/react-reconciler/src/ReactFiberHooks.new.js
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null, // Hook 自身维护的状态
baseState: null, // 上次 render 中第一个被跳过的 update 之前的计算值
baseQueue: null, // 上次 render 未能处理完的更新
queue: null, // Hook 自身维护的更新链表
next: null, // 指向下一个 hook 节点
};
if (workInProgressHook === null) {
// 若当前组件的 Hook 链表为空,就将新建的 Hook 对象作为 Hook 链表的第一个节点(头结点)
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 若当前组件的 Hook 链表不为空,就将新建的 Hook 添加到 Hook 链表的末尾(作为尾结点)
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
// 返回 Hook 链表的指针 workInProgressHook,该指针始终指向最新创建的 Hook 节点
return workInProgressHook;
}
mountWorkInProgressHook 函数中引用的 workInProgressHook 变量和 currentlyRenderingFiber 变量均是ReactFiberHooks.new.js 文件 📃 中的全局变量。其中,workInProgressHook 是正在内存中构建的 Fiber 树中的 Hook 链表的指针,而 currentlyRenderingFiber 这一全局变量初始值为 null,但在 renderWithHooks 函数中进行了赋值,赋值后指向的是正在内存中构建的 Fiber 树。
整个 mountWorkInProgressHook 的逻辑如上注解在代码中,从中可以很清楚可以看到,一个函数组件上的所有Hook 会组成一个单向链表,而链表中的每个节点就是一个 Hook 对象,挂载一个 Hook 意味着创建一个新的链表节点,并将这个新创建的 Hook 节点对象添加到 Hook 链表中。
这里需要特别注意 ⚠️ 的是,Fiber 和 Hook 的数据结构中都有 memoizedState 属性,两者的区分是,Fiber 的 memoizedState 用于保存 Hooks 链表,而 Hook 的 memoizedState 用于保存单一 Hook 对应的数据。
来举个例子,假如现在有一个函数组件,并且第一次执行下面代码:
const [firstName, setFirstName] = useState('张三');
const [lastName, setLastName] = useState('李四');
useEffect(() => {});
由于是第一次执行,也就是在 mount 阶段,会创建如下图的一个 hook 链表:
我们知道,组件状态的变化是通过 Hooks 描述的,那么组件肯定需要一个保存 Hooks 状态的地方,现在我们已经得知 各个 Hook 是以单链表的形式串联在一起最终形成 Hooks 链表 ,那这个 Hooks 链表是怎么和组件关联起来的呢? 该问题本质上是在问整 Hook 链表保存在哪里,其实关于这个问题,在 mountWorkInProgressHook 函数里已经有大致提示了。
if (workInProgressHook === null) {
// 若当前组件的 Hook 链表为空,就将新建的 Hook 对象作为 Hook 链表的第一个节点(头结点)
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
}
若当前组件的 Hook 链表为空,那么就会将新建的 Hook 节点对象作为 Hook 链表的头节点,然后把 Hook 链表的头节点保存在 currentlyRenderingFiber.memoizedState 中,也就是说 Hook 链表是保存在 FiberNode 的memoizedState 属性上的,currentlyRenderingFiber.memoizedState = workInProgressHook = hook这样一个简单的小小的赋值语句,就可以把当前组件和里面的所有 Hook 关联起来。
mountWorkInProgressHook 函数最后会将该 Hook 链表的指针 workInProgressHook 返回 🔙 。对于单链表来说,每执行一次,链表的指针就会后移,始终指向链表中最后一个节点,因此在 mountState 函数中 hook 变量指向的就是当前最新创建的 Hook 节点对象,拿到了新创建的 Hook 节点对象后,就开始对该节点对象进行初始赋值。
简而言之,mountWorkInProgressHook 函数的工作就是创建一个新的 Hook 节点并将该节点添加到组件自身的 Hook 链表上,而 mountState 函数的作用就是对新 Hook 节点进行一系列的初始化工作。
2.3.3 dispatchSetState
到这里,我们肯定还有一个问题很好奇,那就是 useState 怎么处理 state 更新的?
用过 useState 的人都知道,useState 返回一个数组,其中第一个元素是当前 state 的值,而第二个元素是一个用来设置,或者说更新 state 的函数,一般会名为 setXXX。在上面我们看 mountState 源码的时候,可以看到mountState 函数创建了一个 dispatch 变量,并且通过 mountState 函数的返回值我们可以得知,这个 dispatch变量其实就是我们平时所用到的 Hook 更新函数 setXXX,这也说明 dispatch 变量指向的是一个函数,而这个函数就是 dispatchSetState。我们注意到 mountState 还做了一件很关键的事情,绑定当前 fiber 树和 queue 更新队列到 dispatchSetState 上:
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
bind 的使用方法:bind 方法用于创建一个新的函数,这个新函数的 this 会被指定为传递给 bind 的第一个参数,而传递给 bind 的其余参数将作为新函数的参数,供调用时使用。
所以其实当我们调用 useState 的更新方法 setXXX 时,其实本质上就是调用 dispatchSetState 这个方法。那我们当然就要继续往下挖挖,看看 dispatchSetState 的源码:
/**
* @param {*} fiber 当前正在使用的fiber
* @param {*} queue 队列的初始对象
* @param {*} action 更新函数或者要更新的值
*/
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const lane = requestUpdateLane(fiber);
// 为当前更新操作新建 update 对象
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
// 判断当前是否在渲染阶段
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update); // 缓存更新
} else {
enqueueUpdate(fiber, queue, update, lane);
// 优化调度渲染
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// 如果不存在优先级,此前未发生更新,本次是第一个,那就不需要更新了
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
// 上一次的state
const currentState: S = (queue.lastRenderedState: any);
// 获取最新的state
const eagerState = lastRenderedReducer(currentState, action);
// 保存最新的state
update.hasEagerState = true;
update.eagerState = eagerState;
// 浅比较 eagerState 和 currentState,如果两者相等则不开启更新调度,优化性能
if (is(eagerState, currentState)) {
return;
}
}
}
}
const eventTime = requestEventTime();
// 调度渲染当前fiber,scheduleUpdateOnFiber是react渲染更新的主要函数
// 开启调度,触发新的一轮更新,也就是走beginWork,completeWork那一套流程。
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (root !== null) {
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane, action);
}
function isRenderPhaseUpdate(fiber: Fiber) {
const alternate = fiber.alternate;
return (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
);
}
isRenderPhaseUpdate 方法主要用于判断这个更新是否是在渲染过程中产生的。对于传递过来的 fiber 参数,我们层层往上寻找,会发现 fiber参数其实是在创建 dispatchSetState 时 currentlyRenderingFiber 的一个快照值,而在 isRenderPhaseUpdate 函数中的使用的 currentlyRenderingFiber 变量,其实是我们通过 setXXX 执行dispatchSetState 函数时 currentlyRenderingFiber 的快照值。
还记得 renderWithHooks 中,对 currentlyRenderingFiber 的赋值吗?
// `renderWithHooks`
currentlyRenderingFiber = workInProgress;
let children = Component(props, secondArg);
currentlyRenderingFiber = (null: any);
回到 renderWithHooks 源码中,你会发现函数执行的一开始就将 currentlyRenderingFiber 指向了正在内存中构建的 Fiber 树,而在渲染结束 🔚 的时候又将其设置为 null。也就是说,currentlyRenderingFiber 在函数式组件渲染结束后就不存在了。
isRenderPhaseUpdate 方法中的 fiber 参数值在组件挂载阶段创建 dispatchSetState 函数的时候就已经固定不变了,代表的就是正在内存中构建的 Fiber 树,但 currentlyRenderingFiber 的值会随着 dispatchSetState 函数调用时间的不同而发生变化,如果是在组件渲染过程中调用的,那 currentlyRenderingFiber 也指向的是正在内存中构建的 Fiber 树,但如果是在组件渲染完成 ✅ 后才调用,那 currentlyRenderingFiber 就为空。因此如果 fiber === currentlyRenderingFiber,说明这个 setXXX 更新是在当前渲染中产生的,则这是一次 reRender。
如果 isRenderPhaseUpdate (fiber) 的返回值 🔙 为 true 的话,会调用 enqueueRenderPhaseUpdate 函数,因此我们接下来进入到 enqueueRenderPhaseUpdate 源码部分对其进行分析:
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
) {
// This is a render phase update.
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
// Append the update to the end of the list.
// pending 指向的是链表中最新的 update 对象(尾节点)
const pending = queue.pending;
if (pending === null) {
// 第一次更新,那么将当前的更新作为链表的第一个节点,并且让它的 next 属性指向自身,以此来保持为环形链表
// This is the first update. Create a circular list.
update.next = update;
} else {
// 非第一次更新
// 将新 update 对象节点的 next 指针指向链表的头节点,以保持为一个环形链表
update.next = pending.next;
// 通过将链表尾节点的 next 指针指向当前新建的更新对象这种方式把新 update 对象插入链表中
pending.next = update;
}
// 将 queue.pending 指向最新插入的 update 对象节点
queue.pending = update;
}
环状链表的操作可能不太容易理解,我们可以通过一个简单的 demo 来进行详细的分析。
假设现在有三个更新 state 的操作如下:
const [firstName, setFirstName] = useState('');
setFirstName('Lily');
setFirstName('Romeo');
setFirstName('Juliet');
执行 setFirstName('Lily')
const update = {...} // 新建一个 update 更新对象,为了方便说明将其叫做 update0
update.next = update // update0 的 next 指向它自己
queue.pending = update; // queue.pending 指向 update0
执行 setFirstName('Romeo')
const update = {...} // 新建一个 update 更新对象,为了方便说明将其叫做 update1
const pending = queue.pending // 此时的 queue.pending 指向 update0,所以 pending 也指向 update0
update.next = pending.next; // 相当于是 update1.next = update0.next,而 update0.next 指向的是自己,因此最后相当于是 update1.next = update0
pending.next = update; // 相当于 相当于 update0.next = update1
queue.pending = update; // queue.pending 指向 update1
执行 setFirstName('Juliet')
const update = {...} // 新建一个 update 更新对象,为了方便说明将其叫做 update2
const pending = queue.pending // 此时的 queue.pending 指向 update1,所以 pending 也指向 update1
update.next = pending.next; // 相当于 update2.next = update1.next,又因为前面执行 setFirstName('Romeo') 后 update1.next 指向了 update0,所以这一步实际上就是 update2.next = update0
pending.next = update; // 相当于 update1.next = update2
queue.pending = update; // queue.pending 指向 update2
从上面的分析可以看出,函数式组件每执行一次 setXXX 更新函数,都会创建一个 update 更新对象,在渲染过程中的创建的更新对象会以单向环状链表的形式被缓存起来放在 queue.pending 中。你也许会好奇,为什么要把更新操作都保存起来呢,只保存最新的一次更新操作不就行了吗? 这是因为 update 更新操作互相之间是可能存在依赖的,举一个简单的例子🌰:
const [name, setName] = useState('')
setName(name => name + 'a')
setName(name => name + 'b')
setName(name => name + 'c')
如果只保存最新的一次更新操作,拿得到的最新状态值就是'c',这显然不是我们想要的结果,我们期待下次执行时得到 name 的最新状态值应该为'abc',所以每一次更新操作都是不可丢弃的,需要依次执行。
你可能还会疑惑这里为什么要设计为单向环状链表的结构的结构? 这是因为它既有把新的 update 对象节点 push到尾部的需求,又有从头开始遍历链表进行更新的需求。queue.pending 始终指向最后一个插入的 update 对象,而 queue.pending.next 永远指向第一个插入的 update 对象。
我们再回到 dispatchSetState 函数中继续分析后续的逻辑。
由于整个 dispatchSetState 函数的逻辑涉及到Fiber 的调度,本文不详细讲解 Fiber,所以对于和 Fiber 调度相关的内容我们此处只是简单提及。前面我们对 isRenderPhaseUpdate 方法分析后有得出,fiber === currentlyRenderingFiber 时是 reRender,即当前更新周期中又产生了新的更新周期,dispatchSetState 函数根据是否是 reRender 这一判断条件进行了不同的处理。
如果是 reRender,首先会将 didScheduleRenderPhaseUpdate 置为 true,然后再将新创建的 update 更新对象添加到单向环形结构的更新链表的链尾进行缓存,当我们要获取最新的状态时(获取最新状态的相关代码逻辑存在于下文会进行分析的 updateReducer 中)会从头开始遍历链表依次执行每一个 update 对象上的 action,得到最新的state。你可能会问,为什么不是直接执行最后一个 update 更新对象来得到最新的状态?这是因为如果 action 是一个函数,比如 setCount((state)=>{state+1}),下一个 state 值要依赖上一个 state 值,需要都执行一遍才能拿到准确的值,并且 update 更新对象也是存在优先级的,因此不能简单地直接执行最后一个 update 更新对象来得到最新的状态。
如果不是 reRender,那么就会先计算出当前调用 setXXX(action) 传入的 action 值作为新的 state 值并进行保存,然后将这一新的 state 值和上一次渲染的 state 值(即当前屏幕上显示的 state 值 )进行比较,如果有变化就会调用 scheduleUpdateOnFiber 开启新的一次调度安排组件重新渲染,如果没变化就会直接跳过,不安排组件重新渲染。这就证实了为什么 useState,两次值相等的时候组件不重新渲染的原因了。
我们举一个简单的例子🌰:
const [count, setCount] = useState(0);
useEffect(() => {
setCount(1) // 第一个setState,会直接计算state,保存在eagerState中
setCount(2) // 第二个,就不进行计算了,push到queue链表中,留待下次render时执行
}, [])
dispatch 之后,会触发一轮新的 react 更新调度:scheduleUpdateOnFiber(fiber, lane, eventTime),也就是走beginWork --> updateFunctionComponent --> renderWithWork 那一套流程。与组件挂载阶段不同的是,这一次ReactCurrentDispatcher.current = HooksDispatcherOnUpdate,因此在新一轮调度中重新执行那些 hooks 方法时,也就到了 update 阶段。
关于 scheduleUpdateOnFiber 是如何安排更新工作的问题,这里先不展开讲了,因为涉及到另外更多的逻辑和机制,我们后续分析 Fiber 调度原理的时候会单独成文来分析。暂时就先知道关于 setState 这个 hook 有这些提升效率的机制即可~
注意⚠️:didScheduleRenderPhaseUpdate = true 再一次和前面的 renderWithHooks 函数关联起来,因为在renderWithHooks 中,如果 didScheduleRenderPhaseUpdate 为 true,就会循环计数 numberOfReRenders来记录 re-render 的次数,如果超过一定的次数,就会报出 Too many re-renders 的错误,另外nextWorkInProgressHook 也会有值,所以后续的代码中,有用 numberOfReRenders > 0 来判断是否是 reRender 的,也有用 nextWorkInProgressHook是否为空来判断是否是 reRender 的。
2.4 组件更新
2.4.1 updateState
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
可以发现,其实 updateState 最终调用的其实是 updateReducer。回顾 2.2.4 HooksDispatcherOnMount & HooksDispatcherOnUpdate 小节,updateReducer 实际上是我们平时使用的 useReducer 在更新阶段所调用的方法。
useReducer 是 React 提供的一个高级 Hook,它不像 useEffect、useState、useRef 等必须 Hook 一样,没有它我们也可以正常完成需求的开发,但 useReducer 可以使我们的代码具有更好的可读性、可维护性、可预测性。如果对 useReducer 的基础使用较熟悉的话,一定会知道 useReducer 接受一个 reducer 参数,该参数的类型为 (state, action) => newState,表示的含义为接收当前应用的 state 和触发的动作 action,计算并返回最新的 state 值。
因为我们调用 useState 的时候不会传入 reducer,所以这里会默认传一个 basicStateReducer 进去作为reducer。也就是说,useState 其实只是 useReduer 的一个特殊情况而已,useState 使用的是 react 帮我们已经封装好的 basicStateReducer,而非 useReducer 那样使用的 reducer 是开发者传入的自定义 reducer。
// action 为更新函数或者要更新的值
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
可以看到,basicStateReducer 跟 useReducer 的所需的 reducer 参数具有相同的函数签名的: (state, action) => newState,由此证明 basicStateReducer 也是一个 reducer。在使用 useState 时其实我们的 action 通常是一个值,而不会是一个函数,所以其实 basicStateReducer 就会直接把这个值返回出来而已。那么接下来当然就要来看看 updateReducer 的代码啦~
2.4.2 updateReducer
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 获取正在执行的处于更新阶段 Hook 节点
const hook = updateWorkInProgressHook();
// 获取该 Hook 节点的更新链表
const queue = hook.queue;
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
// queue.lastRenderedReducer 用于得到最新 state
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
// The last rebase update that is NOT part of the base state.
// hook.baseQueue 是存储低于本次渲染优先级的 update 对象的环形链表.
// 即上一次计算新 state 之后,剩下的优先级低的更新.
// 或者说上次 render 未能处理完的 update.
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
// queue.pending 是更新队列的最后一个 update 对象
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
// 把 pendingQueue 链接到 baseQueue 的尾部,以便一起迭代执行
// 下面的这些操作就是把循环链表剪开,然后拼接在一起
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
// 清空更新环形链表
queue.pending = null;
}
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
// 遍历链表,计算 state
do {
// 获取更新对象的优先级
const updateLane = update.lane;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 优先级不足,先标记,后面再更新
// Priority is insufficient. Skip this update.
// If this is the first skipped update, the previous update/state is the new base update/state.
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
// 把这个 update 添加到下次的 baseQueue 中,留待下次 render 时执行
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
// baseState 会停留在被跳过的 update 之前的计算值
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.
if (newBaseQueueLast !== null) {
// newBaseQueueLast 存在证明此前有 update 被跳过
// 因为 update 互相之间可能存在依赖,所以从那个被跳过的 update 起,所有后面的 update 都链接到 newBaseQueueLast 中
// 在下次 render 一齐执行
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Process this update.
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
// eagerReducer 不为 null,证明 dispatch 的时候,使用过这个 reducer 来计算过 state 值,所以 eagerReducer 和 eagerState 都被赋值了
// 再检查一下上个 reducer 和当前的 reducer 有没有改变,没有的话就可以直接使用其计算出来的值
// 简单的说就是状态已经计算过,那就直接用
newState = ((update.eagerState: any): S);
} else {
// action 就是传的参数,例如 setState('参数')
const action = update.action;
// 计算新状态
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 如果有 update 被跳过,那么 memoizedState 与 baseState 会是不同的值
// baseState 会停留在被跳过的 update 之前的计算值
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
// Interleaved updates are stored on a separate queue. We aren't going to
// process them during this render, but we do need to track which lanes
// are remaining.
const lastInterleaved = queue.interleaved;
if (lastInterleaved !== null) {
let interleaved = lastInterleaved;
do {
const interleavedLane = interleaved.lane;
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
interleavedLane,
);
markSkippedUpdateLanes(interleavedLane);
interleaved = ((interleaved: any).next: Update<S, A>);
} while (interleaved !== lastInterleaved);
} else if (baseQueue === null) {
// `queue.lanes` is used for entangling transitions. We can set it back to
// zero once the queue is empty.
queue.lanes = NoLanes;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
这一段看起来很复杂,让我们慢慢吃透,会发现实际上核心逻辑就是 updateReducer 会去遍历更新链表,执行每一个节点里面的更新操作,得到最新的状态并返回,以此来保证我们每次刷新组件都能拿到当前最新的状态。之所以看起来比较复杂是因为要实现这两个功能:
- 我们的更新任务可能是异步的,也就是说前一个任务的执行可能被打断,让位给更高优先级的任务,这个时候,需要保证低优先级的任务的状态不会丢失。
- 由于更新队列中,优先级低的 update 会被暂缓执行,只执行高优先级的任务,并且互相的 update 是有可能相互依赖的,所以需要保证最后所有的任务执行完毕时,update 的顺序不被打乱。
updateReducer 为了实现这两个功能,会对每个更新进行优先级判断,如果不是当前整体更新优先级内的更新会跳过,第一个跳过的 Update 会变成新 baseUpdate 上的第一个节点,并且从那个被跳过的 update 起,所有后面的 update 都会被连接 🔗 到 baseUpdate 中,即便是优先级比它高,这是因为 update 互相之间可能存在依赖,在第一个跳过的更新被执行的时候,需要保证后续的更新要在它更新之后的基础上再执行,不然的话结果可能会不一样。也就是说,进行优先级判断之后得到的 baseUpdate 实际上存储的就是低于本次渲染优先级的update 对象循环链表,虽然这些更新任务的优先级不足,但并不代表它们应该被丢弃,相反它们只是需要暂缓执行,然后在下次 render 中,再从被跳过的 update 开始执行任务,保证其中顺序不变。为此,updateReducer 通过将 pendingQueue 和 baseQueue 这两个循环链表剪开,把 pendingQueue 链接到 baseQueue 的尾部,然后拼接在一起形成本次 render 最终的更新循环 ♻️ 链表,保证了低优先级的任务的状态不会丢失。
2.4.3 updateWorkInProgressHook
在分析 updateWorkInProgressHook 之前,我们先加深一下 currentHook 和 workInProgressHook 这两个全局变量代表的含义,这有助于帮我们快速的理清楚 updateWorkInProgressHook 的逻辑。 2.4 小节对组件挂载阶段的源码进行分析后,我们得出的结论为:各个 Hook 以单链表的形式串联在一起最终形成一个 Hooks 链表保存在 FiberNode 的 memoizedState 属性上。与 Fiber 树按照状态分为当前屏幕上显示内容对应的 currentFiber 树和正在内存中构建的 workInProgressFiber 树相对应,React 维护的 Hooks 链表也分为 current hook list 和work-in-progress hook list。顾名思义,current hook list 就是保存在currentFiber 树中的 Hooks 链表,而work-in-progress hook list 是保存在 workInProgressFiber 树上的Hooks 链表。
currentHook 实际上就是 current hook list 的游标(指针),用来对 current hook list 链表进行遍历,始终指向与当前被调用(正在执行)的 Hook 对应的 Hook 对象。比较特殊的是,由于 Hooks 链表是在 mount 组件挂载阶段创建的,因此在 mount 阶段的执行过程中 currentHook 实际上始终指向 Hooks 链表中最后一个节点,即最新创建的 Hook 节点对象。同理,workInProgressHook 实际上就是 current hook list 的游标,用来对 work-in-progress hook list 链表进行遍历。
明白了 currentHook 和 workInProgressHook 的具体含义后,我们就可以开始梳理函数的具体逻辑啦~
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook;
// 初始化 nextCurrentHook
if (currentHook === null) {
// currentHook 在函数组件调用完成时会被设置为 null
// 这说明组件是刚刚开始重新渲染,刚刚开始调用第一个hook 函数
// currentlyRenderingFiber 指向正在内存中构建的 Fiber 树
// 将 current 指向当前屏幕上显示内容对应的 Fiber 树
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
// 将 nextCurrentHook 指向当前屏幕上显示内容对应的 Fiber 树的 hook 链表的第一个节点
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// currentHook 有数据不为空,这说明已经不是第一次调用 hook 函数了
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook: null | Hook;
// 初始化 nextWorkInProgressHook
if (workInProgressHook === null) {
// workInProgressHook 在函数组件调用完成时会被设置为 null
// 所以当前的判断分支说明现在 hooks 链表为空,正调用第一个 hook 函数
// currentlyRenderingFiber === workInProgress
// workInProgress.memoizedState 在函数组件每次开始渲染时都会被初始化设置成 null
// 将 nextWorkInProgressHook 指向 workInProgress.memoizedState,为 null
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 走到这个分支说明 hooks 链表已经有元素了
// 将 nextWorkInProgressHook 指向 hooks 链表的下一个元素
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
// 这是组件重渲染的情况,我们暂时先不考虑
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
if (nextCurrentHook === null) {
throw new Error('Rendered more hooks than during the previous render.');
}
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
// 这里要注意,nextWorkInProgressHook 为空,workInProgressHook 不一定空。
// 因为初始化后 nextWorkInProgressHook 仍然为空分两种情况:
// 1.workInProgressHook 为空,并且 currentlyRenderingFiber.memoizedState 为空,即 Hook 链表为空;
// 2.workInProgressHook 不为空,当前指针指向了链表的尾部,所以 workInProgressHook.next 为空,导致 nextWorkInProgressHook 为空。
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
源码可能看起来有点迷糊,它的逻辑大致是这样的:
在组件进行更新时,会在内存中构建一颗新的 Fiber 树,即 workInProgress 。我们前面有说过,各个 Hook 以单链表的形式串联在一起最终形成一个 Hooks 链表保存在 FiberNode 的 memoizedState 属性上。但是在函数组件每次开始渲染时,workInProgress.memoizedState 都会被初始化设置成 null,那么正在内存中构建的 Fiber 树要如何获取 hooks 链表呢?
// renderWithHooks
workInProgress.memoizedState = null;
实际上 workInProgress fiber 需要从 current fiber 来获取 hooks 链表。也就是根据当前屏幕上显示内容对应的Fiber 树 ,拿到之前已经创建好的 Hook,再复制一份到 workInProgress 上,生成一个新的 Hooks 链表。而之前已经创建好的 Hook 是怎么拿的呢?是去遍历 Hooks 链表拿的,所以每次都会按顺序拿下一个 Hook ,然后复制一份挂载到 workInProgress fiber 的 memoizedState 中,因此可以理解为 updateWorkInProgressHook 每次都会按顺序返回下一个 Hook 。
这里值得注意 ⚠️ 的是,在正常情况下,一次 renderWithHooks 执行,workInProgress 上的 memoizedState 会被置空,Hooks 函数按照顺序执行,nextWorkInProgressHook 应该一直为 null。但是有个例外,那就是组件Re-render 的情况,因为在之前的 render 中已经创建 Hooks 链表了,所以在 Re-render 的情况下正在内存中构建的 workInProcess Fiber 树仍然存在Hooks 链,因此 nextWorkInProgressHook 不为 null。
Re-render 的情况的处理主要体现在下面 ⬇️ 这两段代码中:
组件正常更新,刚刚开始调用第一个 hook 函数时会进入到第一个 if 判断分支,后续则会进入 else 分支,单就算是进入 else 分支,nextWorkInProgressHook 也一直为 null,因为 workInProgress 的 Hooks 链表还没有完全复制过来,在组件执行的过程中,是每遇到一个 Hook,就将该 Hook 从 current fiber 中复制过来,并不是一次性将整个 Hooks 链表拷贝过来,所以 workInProgressHook 始终指向 workInProgress Hooks 链表的尾节点。而只有在 Re-render 的情况,不仅 workInProgress Hooks 链表存在,并且已经完全创建好,走 else 分支的时候得到的 nextWorkInProgressHook 值才不为 null(已经遍历到尾节点除外)。
if (workInProgressHook === null) {
// 成功获取到hooks链表
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 走到这个分支说明 hooks 链表已经有元素了
// 将 nextWorkInProgressHook 指向 hooks 链表的下一个元素
nextWorkInProgressHook = workInProgressHook.next;
}
对于 Re-render 组件重渲染,是不用再重新进行 Clone from the current hook 操作的,直接迭代就行:
if (nextWorkInProgressHook !== null) {
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
}
2.5 组件重渲染
在 2.2.3 小节对 renderWithHooks 的分析中,我们重点关注的主要是函数式组件在挂载阶段和更新阶段上对ReactCurrentDispatcher.current 的赋值。实际上,renderWithHooks 对组件重渲染也进行了相应的处理,组件重渲染就是在当前更新周期内又发生了更新,此时会直接 Re-render 重新执行,直到不产生新的更新为止。对于这种场景,ReactCurrentDispatcher.current 会被赋值为 HooksDispatcherOnRerender 。
2.5.1 HooksDispatcherOnRerender
const HooksDispatcherOnRerender: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: rerenderReducer,
useRef: updateRef,
useState: rerenderState,
useDebugValue: updateDebugValue,
useDeferredValue: rerenderDeferredValue,
useTransition: rerenderTransition,
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useId: updateId,
unstable_isNewReconciler: enableNewReconciler,
};
如果将 HooksDispatcherOnMount 和 HooksDispatcherOnUpdate 放在一起进行对比,你会发现其实有些Hook 在组件更新阶段和组件重渲染阶段的逻辑是一致的,因为都是调用的同一个函数,只有部分 Hook 在这两个阶段的处理逻辑会有不同,其中就包括 useState 这个 Hook。前面我们对 useState 在组件挂载和组件更新阶段的逻辑进行了分析,那么接下来我们继续对 useState 在组件重渲染阶段的逻辑进行分析。
2.5.2 rerenderState
function rerenderState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return rerenderReducer(basicStateReducer, (initialState: any));
}
2.5.3 rerenderReducer
function rerenderReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 获取正在执行的处于更新阶段 Hook 节点
const hook = updateWorkInProgressHook();
// 获取该 Hook 节点的更新链表
const queue = hook.queue;
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
// queue.lastRenderedReducer 用于得到最新 state
queue.lastRenderedReducer = reducer;
// This is a re-render. Apply the new render phase updates to the previous
// work-in-progress hook.
const dispatch: Dispatch<A> = (queue.dispatch: any);
// queue.pending 是更新链表的最后一个 update 对象
const lastRenderPhaseUpdate = queue.pending;
// 当前状态值
let newState = hook.memoizedState;
if (lastRenderPhaseUpdate !== null) {
// The queue doesn't persist past this render pass.
queue.pending = null;
// 获取更新链表的第一个 update 对象,从第一个更新对象开始执行更新
const firstRenderPhaseUpdate = lastRenderPhaseUpdate.next;
let update = firstRenderPhaseUpdate;
// 循环链表执行更新
do {
// Process this render phase update. We don't have to check the
// priority because it will always be the same as the current
// render's.
// action 就是传的参数,例如 setState('参数')
const action = update.action;
// 计算新状态
newState = reducer(newState, action);
// 指向下一个更新对象
update = update.next;
} while (update !== firstRenderPhaseUpdate);
// Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
// Don't persist the state accumulated from the render phase updates to
// the base state unless the queue is empty.
// TODO: Not sure if this is the desired semantics, but it's what we
// do for gDSFP. I can't remember why.
if (hook.baseQueue === null) {
hook.baseState = newState;
}
queue.lastRenderedState = newState;
}
return [newState, dispatch];
}
3. 设计思路
ES Module 采用的是实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化,也就是说,对于 ES Module 来说,如果源模块的值发⽣变化,会连带影响当前引⼊的值。
React.js 中导出的 Hook 都是挂载在一个叫 Dispatcher 的调度器上的,React 函数式组件在渲染时会从该调度器中寻找需要的 Hook。这个 Dispatcher 调度器就是 ReactCurrentDispatcher.current,根据 ES Module 的原理可知,只要 ReactCurrentDispatcher.current 的值发生变化,那么我们在项目代码中引入的 Hook 就会发生变化。
利用这一特点,React 团队的工程师们根据组件的挂载、更新、重渲染这 3 种不同的场景分别设计了 3 个不同的Dispatcher,HooksDispatcherOnMount、HooksDispatcherOnUpdate 和 HooksDispatcherOnRerender,把Hooks 在 Mount 挂载阶段的逻辑存到 HooksDispatcherOnMount 对象中,把 Update 更新阶段的逻辑存到HooksDispatcherOnUpdate 对象中,把 Rerender 重渲染阶段的逻辑存到 HooksDispatcherOnRerender 对象中, 并以每个 Hook 的名字 useXXX 作为对象的 key 值。
React 函数式组件在每次渲染之前,根据上下文来判断这是一次组件挂载、组件更新还是组件重渲染,然后将ReactCurrentDispatcher.current 赋值为三种 Dispatcher 中对应的那一个。以此来实现虽然我们明面上调用的是useXXX(包括但不限于 useEffect、useCallback、useState)这些 Hook,但是对于不同的更新模式,实际执行的逻辑是不一样的这一功能。
这就是整个 Hooks 的设计思路。
4. 附录
React源码地址(版本:v18.0.0)