主题
React Hooks 源码分析
按源码调用链梳理函数组件挂载与更新时的 Hooks 执行机制:
renderWithHooks如何在两套调度器之间切换、Hook 节点如何串成链表、useState的更新队列如何入队与回放、useEffect的依赖对比与 tag 标记,以及useMemo、useRef的缓存实现。源码位置:react-reconciler/src/ReactFiberHooks.js(expirationTime调度时期的实现)。
一、renderWithHooks
函数组件初始化(mount)和更新(update)时都会调用该函数。
js
const NoWork = 0;
export function renderWithHooks(
current: Fiber | null, // 渲染树,在 mount 阶段值为空
workInProgress: Fiber, // 内存中构建的树
Component, // 函数组件本身
props,
secondArg,
nextRenderExpirationTime: number,
) {
renderExpirationTime = nextRenderExpirationTime;
// 用于后续更新时判断传入的节点是否是当前正在渲染的节点
// 浅拷贝,currentlyRenderingFiber 与 workInProgress 指向同一内存地址
currentlyRenderingFiber = workInProgress;
// 置空 workInProgress 上的 memoizedState 与 updateQueue,
// 后续会把新的 hooks 信息挂载到这两个属性上
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.expirationTime = NoWork;
// mount 阶段使用 HooksDispatcherOnMount
// update 阶段使用 HooksDispatcherOnUpdate
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 执行函数组件
let children = Component(props, secondArg);
if (workInProgress.expirationTime === renderExpirationTime) {
// 检查是否有渲染阶段更新,这里的逻辑先放一放
}
// 判断 hooks 是否在函数组件内部执行,否则捕获并抛出异常
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
renderExpirationTime = NoWork;
// 函数执行完毕,将 currentlyRenderingFiber 置空
currentlyRenderingFiber = null;
// 分别将 current 树和 workInProgress 树上指向当前 hooks 节点的字段置空
currentHook = null;
workInProgressHook = null;
// 暂停更新标识
didScheduleRenderPhaseUpdate = false;
return children;
}关键变量:
current:渲染树。workInProgress:内存中构建的树。组件更新时会复制 current 树作为 workInProgress 树,在组件 commit 阶段再把 workInProgress 赋值给 current。workInProgress.memoizedState:在类组件中存放 state 信息;在函数组件中以链表的形式存放 hooks 信息。workInProgress.expirationTime:确定更新的优先级(number 类型)。currentHook:指向 current 树当前调度的 hooks 节点。workInProgressHook:指向 workInProgress 树当前调度的 hooks 节点。
二、ReactCurrentDispatcher
在 renderWithHooks 函数中,通过判断 current 的值来调用不同的调度器。
挂载阶段的调度器:
js
// 常用的 hooks
const HooksDispatcherOnMount = {
useCallback: mountCallback,
useEffect: mountEffect,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
};更新阶段的调度器:
js
const HooksDispatcherOnUpdate = {
useCallback: updateCallback,
useEffect: updateEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
};对于第一次渲染组件和更新组件,react-hooks 采用了两套 API:挂载阶段采用 HooksDispatcherOnMount,更新阶段采用 HooksDispatcherOnUpdate。
三、mountWorkInProgressHook
组件初始化的时候,每一次 hooks 执行都会调用 mountWorkInProgressHook 函数。
源码位置:react-reconciler/src/ReactFiberHooks.js → mountWorkInProgressHook
js
function mountWorkInProgressHook() {
const hook: Hook = {
memoizedState: null,
baseState: null, // 一次更新中,产生的最新 state 值
baseQueue: null, // useState 和 useReducer 中保存最新的更新队列,Update 类型
queue: null, // 保存待更新队列
next: null, // 指向下一个 hook 对象
};
// 例子中的第一个 hooks -> useState(0) 走的就是这样
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}memoizedState 在不同 Hook 中保存的内容不同:
useState:保存 state 信息useEffect:保存 effect 对象useMemo:保存缓存的值和 depsuseRef:保存 ref 对象
通过链表的形式将 hooks 串联起来:currentlyRenderingFiber.memoizedState 指向链表的头节点,workInProgressHook 指向链表当前调度的节点。
因为是浅拷贝,所以这里 currentlyRenderingFiber.memoizedState 等同于 workInProgress.memoizedState。
示意图占位:原笔记此处有两张图(「第一步」「最后一步」),对比 hook 逐个入链前后的链表状态。
四、mountState
js
function mountState(initialState) {
const hook = mountWorkInProgressHook();
if (typeof initialState === "function") {
// 如果 useState 第一个参数为函数,执行函数得到 state
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null, // 待更新的
dispatch: null, // 负责更新函数
lastRenderedReducer: basicStateReducer, // 用于得到最新的 state
lastRenderedState: initialState, // 最后一次得到的 state
});
const dispatch = (queue.dispatch = dispatchAction.bind(
// 负责更新的函数
null,
currentlyRenderingFiber,
queue,
));
return [hook.memoizedState, dispatch];
}将初始化的 state 赋值给 mountWorkInProgressHook 产生的 hook 对象的 memoizedState 和 baseState 属性,然后创建一个 queue 对象,里面保存了负责更新的信息。
dispatchAction 的结构:
js
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
)以 const [number, setNumber] = useState(0) 为例,dispatchAction 就是 setNumber:它的第一个参数和第二个参数已经被 bind 改为 currentlyRenderingFiber 和 queue,我们传入的参数是第三个参数 action。
五、dispatchAction
1. 创建 update 对象
js
function dispatchAction(fiber, queue, action) {
// 第一部分
// 创建一个 update 对象
const update = {
expirationTime,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
/* 把创建的 update 加入更新队列 */
const pending = queue.pending;
if (pending === null) {
// 如果更新队列为空,那么将当前更新作为更新队列的第一个节点
// 并让它的 next 属性指向自身
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
// 让 queue.pending 指向最新的节点
queue.pending = update;
}假设有连续 3 个更新状态的操作:
js
setNumber("0");
setNumber("1");
setNumber("2");示意图占位:原笔记此处有三张图(1. / 2. / 3.),分别对应三次
setNumber之后的更新队列形态。
最终的 hooks 链表结构:在 Hook 链表上的 useState Hook 节点,通过循环链表的形式存放着历史更新操作,其中 queue.pending 一直指向最新一次的更新操作。
2. 判断是否更新是组件到达 update 阶段
dispatchAction 将当前传入的值(action)和此时显示在屏幕上的值(currentState)进行浅比较,相等则退出,否则调用 scheduleWork 安排 fiberNode 的更新工作。
js
function dispatchAction(fiber, queue, action) {
// 第二部分
const alternate = fiber.alternate;
/* 判断当前是否在渲染阶段 */
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
didScheduleRenderPhaseUpdate = true;
update.expirationTime = renderExpirationTime;
currentlyRenderingFiber.expirationTime = renderExpirationTime;
} else {
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
try {
const currentState = queue.lastRenderedState; /* 上一次的 state */
const eagerState = lastRenderedReducer(currentState, action);
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
return;
}
} catch (error) {
// 抑制错误,render 阶段会再次抛出
}
}
}
scheduleWork(fiber, expirationTime);
}
}scheduleWork 并非马上让组件重新渲染,其中还有各种更新优先级的判断处理。例如这里的 useState,要等到当前所有的 setXxx() 都逐一执行完了,最终才会集中一次更新。
六、updateWorkInProgressHook
alternate:React 通过双缓冲树保存状态,一颗保存当前状态 current,一颗保存上一次状态 workInProgress,两颗 fiber 树用 alternate 相互指向。
updateWorkInProgressHook 的作用:
- 生成
workInProgressHook,获取正处于执行更新阶段的 hook - 更新全局
currentHook
js
function updateWorkInProgressHook() {
let nextCurrentHook;
if (currentHook === null) {
/* 如果 currentHook === null,证明它是第一个 hooks */
// currentlyRenderingFiber 等于内存中构建的树 workInProgress
// workInProgress.alternate 等于 current 树
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
// 将当前节点树的 hooks 信息存入 nextCurrentHook
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
/* 不是第一个 hooks,那么指向下一个 hooks */
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook;
if (workInProgressHook === null) {
// 第一次执行 hooks
// 这里应该注意一下,当函数组件更新也是调用 renderWithHooks,
// memoizedState 属性是置空的
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
/* 这个情况说明 renderWithHooks 执行过程发生多次函数组件的执行 */
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
invariant(
nextCurrentHook !== null,
"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,
};
if (workInProgressHook === null) {
// 如果是第一个 hooks
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// 重新更新 hook
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}逻辑:
- 首先如果是第一次执行 hooks 函数,那么从 current 树上取出
memoizedState。 - 然后声明变量
nextWorkInProgressHook。正常情况下一次renderWithHooks执行,workInProgress 的memoizedState会被置空,nextWorkInProgressHook应该一直为 null。如果当前组件执行后还是处于渲染优先级,说明函数组件又有了新的更新任务,这就造成上述的不为 null 的情况。 - 最后复制 current 的 hooks,把它赋值给
workInProgressHook,用于更新新一轮的 hooks 状态。
七、updateState
组件在第一次渲染时使用 mountState,之后渲染执行 updateState:
js
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}updateState 实际调用的是 updateReducer,这也证实了 useState 实际是简化版的 useReducer:调用 useState 的时候不会传入 reducer,因为这里会默认传一个 basicStateReducer 进去作为 reducer。
js
// 默认 reducer
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}action 是函数则执行,否则直接返回 action。
js
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;
// ...
// 获取更新队列最初的 update 对象节点
// hook.baseQueue 存储最新的 update 节点,.next 则是最初的 update 节点
let first = currentHook.baseQueue.next;
// 初始化 newState
let newState = currentHook.baseState;
// ...
let update = first;
// ...
do {
// ...
// 循环遍历更新队列链表
// 从最早的那个 update 对象开始遍历,每次遍历执行一次更新,去更新状态
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
// ...
// 返回最新的状态和修改状态的方法
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}注意:上面的
...是原笔记省略的部分,不是可直接运行的完整代码。
执行步骤:
- 获取正在执行的处于更新阶段 Hook 节点。
- 获取该 hook 节点的更新队列链表。
- 从该更新队列最早的 update 对象节点开始遍历,一直遍历到最新添加的 update 对象节点,遍历到每个节点的时候执行该节点的更新操作,将该次更新的 state 值存到
newState中。 - 当遍历完最近的一个 update 对象节点后,此时
newState里存放的就是最新的 state 值,最后返回newState,于是用户拿到最新的 state。
八、useState 运行流程
1. 组件初次渲染时
- 在 hooks 链表上添加该 useState 的 Hook 节点。
- 初始化 state 值。
- 返回此次渲染的 state 和修改 state 的方法。
2. 调用 setXxx / dispatchAction 时
- 创建 update 对象,并将 update 对象添加到该 hook 节点的更新队列链表。
- 判断传入的值(
action)和当前正在屏幕上渲染的 state 值是否相同(浅比较):相同则return;不相同则调用scheduleWork安排组件的重新渲染。 - 当所有
setXxx都逐一执行完后,假设能满足第 2 步的条件,则触发更新(组件重新渲染),进入 update 阶段。
3. 组件更新时
- 获取该 useState Hook 的更新队列链表。
- 遍历这个更新队列链表,从最早的那个 update 对象开始遍历,直至遍历到最新添加的那一个 update 对象,最后得到最新的 state 并返回,作为此次组件渲染的 state。
- 返回此次渲染的 state 和修改 state 的方法。
九、mountEffect
js
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// ...
return mountEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
);
}effect 的 tag:
HookHasEffect:表示该 effect 需要执行,如果没有这个 tag,那么这个 effect 将会被跳过,不会执行。HookPassive:表示如果这个 effect 需要被执行,那么它将会在组件 UI 渲染完后执行(paint 之后)。HookLayout:表示如果这个 effect 需要被执行,那么它将会在组件 UI 渲染之前执行(layout 之后,paint 之前)。
mountEffect 只是作为一个入口,真正开始干活的是 mountEffectImpl 函数:
js
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 获取当前 Hook 节点,同时将当前 Hook 添加到 Hook 链表中
const hook = mountWorkInProgressHook();
// 获取依赖
const nextDeps = deps === undefined ? null : deps;
// 将当前 effect 对象保存到 hook.memoizedState 中
// 以及添加到 fiberNode 的 updateQueue 上
hook.memoizedState = pushEffect(
HookHasEffect | hookEffectTag,
create,
undefined,
nextDeps,
);
}pushEffect 的作用是将当前 effect 添加到 FiberNode 的 updateQueue.lastEffect 中:
js
function pushEffect(tag, create, destroy, deps) {
const effect = {
tag,
create, // 副作用函数
destroy, // 清除副作用函数
deps, // 依赖项
next, // 指向下一个 effect 对象
};
// 获取当前 FiberNode 的 updateQueue
let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
if (componentUpdateQueue === null) {
// 如果 updateQueue 为空,则创建一个新的 updateQueue
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
// updateQueue.lastEffect 指向最新添加进来的 effect
// 保持循环链表的结构
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
// 假如 lastEffect 指向 null,说明此时链表还不是循环链表的结构,
// 那么就要控制最新的 effect 的 next 的指向,使其变为循环链表的结构
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 将当前 effect 添加到 updateQueue 中
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
// 令 lastEffect 始终指向最新添加进来的 effect
componentUpdateQueue.lastEffect = effect;
}
}
// 返回当前 effect
return effect;
}updateQueue.lastEffect 属性指向的始终是最新添加进队列的 effect,lastEffect.next 始终指向最早添加进来的 effect,以此形成一个循环链表的结构。(updateQueue 用于存储 effect 的循环链表,memoizedState 用于存储 hook 的单向链表。)
十、updateEffect
js
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// ...
return updateEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
);
}updateEffect 的调用流程和 mountEffect 相似,updateEffect 只是一个入口,真正干活的是 updateEffectImpl:
js
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 获取当前 Hook 节点,并把它添加到 Hook 链表中
const hook = updateWorkInProgressHook();
// 获取依赖
const nextDeps = deps === undefined ? null : deps;
// 初始化清除 effect 函数
let destroy = undefined;
if (currentHook !== null) {
// 获取上一次渲染的 Hook 节点的 effect
const prevEffect = currentHook.memoizedState;
// 获取上一次渲染的 Hook 节点的 effect 的清除函数
destroy = prevEffect.destroy;
if (nextDeps !== null) {
// 获取上一次渲染的 Hook 节点的 effect 的依赖
const prevDeps = prevEffect.deps;
// 对比前后依赖的值是否相同
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果依赖的值相同,即依赖没有变化,
// 那么只会给这个 effect 打上一个 HookPassive tag,
// 然后在组件渲染完以后会跳过这个 effect 的执行
pushEffect(hookEffectTag, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.effectTag |= fiberEffectTag;
// pushEffect 的作用是将当前 effect 添加到 FiberNode 的 updateQueue 中,
// 然后把返回的当前 effect 保存到 Hook 节点的 memoizedState 属性中
hook.memoizedState = pushEffect(
HookHasEffect | hookEffectTag,
create,
destroy,
nextDeps,
);
}update 阶段的 useEffect 和 mount 阶段所做的事情基本相似,唯独不一样的就是 update 阶段会考虑 effect 的依赖是否有变化:
- 如果没有变化,那么只会给这次 effect 打上
HookPassivetag,在最后 commit 阶段(组件视图渲染完成后)会跳过 effect 的执行。 - 如果依赖有变化,那么就会给这次 effect 打上
HookPassive和HookHasEffect两个 tag,表示这个 effect 将会在组件视图渲染完成后执行。
小结:
- 获取当前 hook 节点,并把它添加到 hook 链表中。
- 获取本次 effect 的 deps 依赖。
- 拿本次 effect 的依赖和上一次渲染时的 effect 的依赖做对比:
- 假如依赖没有发生改变,那么就只给这次 effect 打上
HookPassive的 tag,在 commit 阶段(组件视图渲染完成后)跳过这一次 effect 的执行; - 假如依赖有发生改变,那么就会给这次 effect 打上
HookPassive和HookHasEffect两个 tag,在 commit 阶段(组件视图渲染完成后)执行这一次 effect。
- 假如依赖没有发生改变,那么就只给这次 effect 打上
- 将本次 effect 添加到 fiberNode 的 updateQueue 中,并将本次 effect 保存在当前 Hook 节点的
memoizedState中。
十一、useEffect 运行流程
1. 组件初次渲染
- 执行 useEffect 时,将 useEffect Hook 添加到 Hook 链表中,然后创建 fiberNode 的 updateQueue,并把本次 effect 添加到 updateQueue 中。
- 渲染组件的 UI。
- 完成 UI 渲染后,执行本次 effect。
2. 组件重新渲染
- 执行 useEffect 时,将 useEffect Hook 添加到 Hook 链表中,判断依赖:
- 假设没有传入依赖,那么直接给这个 effect 打上「需要执行」的 tag。
- 假如有传入依赖 deps,并且当前依赖和上次渲染时的依赖对比有发生改变,那么就给这个 effect 打上「需要执行」的 tag。
- 假如有传入依赖 deps,但是依赖没有发生改变,则不会给这个 effect 打上「需要执行」的 tag。
- 假如有传入依赖 deps,但是传入的是一个空数组
[],那么也不会给这个 effect 打上「需要执行」的 tag。
- 渲染组件的 UI。
- 假如有清除函数(effect 中的
return内容),则执行上一次渲染的清除函数;如果依赖是[],则先不用执行清除函数,而是等到组件销毁时才执行。 - 判断本次 effect 是否有「需要执行」的 tag(
HookHasEffect):如果有,就执行本次 effect;如果没有,就直接跳过,不执行本次 effect。
3. 组件销毁时
- 在组件销毁之前,先执行完组件上次渲染时的清除函数。
十二、mountMemo
js
function mountMemo(nextCreate, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}初始化 useMemo,就是创建一个 hook,然后执行 useMemo 的第一个参数,得到需要缓存的值,然后将值和 deps 记录下来,赋值给当前 hook 的 memoizedState。
十三、updateMemo
js
function updateMemo(nextCreate, deps) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps; // 新的 deps 值
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1]; // 之前保存的 deps 值
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 判断两次 deps 值
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}在组件更新中,判断两次 deps 是否相等:如果不相等,证明依赖项发生改变,那么执行 useMemo 的第一个函数,得到新的值,然后重新赋值给 hook.memoizedState;如果相等,那么直接获取缓存的值。
十四、mountRef
js
function mountRef(initialValue) {
const hook = mountWorkInProgressHook();
const ref = { current: initialValue };
hook.memoizedState = ref;
return ref;
}创建一个 ref 对象,对象的 current 属性用来保存初始化的值,最后用 hook.memoizedState 保存 ref。
十五、updateRef
js
function updateRef(initialValue) {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}返回缓存的值,也就是无论函数组件执行多少次,hook.memoizedState 内存中都指向同一个对象。