在使用React开发一段时间后,经常接触到Hooks的使用,对其中的一些Hooks设计比较感兴趣,所以这篇文章围绕使用场景最为频繁的useState来进行拆解分析。
场景分析
以下代码是我们在开发场景中经常会遇到的,通过引入useStateHook函数来实现函数组件内的状态管理,如果你了解其Fiber架构的话,就会知道在组件挂载阶段会创建该App组件对应的Fiber下的Hook对象,其绑定在Fiber节点的memoizedState属性上,其存储了该Fiber下对应的Hook对象链表,通过遍历该链表可以获得对应的Hook对象,所以我们当前组件下也是按顺序建立链表连接。
js
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0);
const add = () => {
setCount(count + 1)
}
return (
<div>
<h1>useState Hook底层原理分析</h1>
<button onClick={add}>+1</button>
</div>
)
}
底层原理
在底层实现中,其函数定义如下:
js
export function useState(initialState) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
首先通过调用resolveDispatcher函数返回我们的全局对象ReactCurrentDispatcher.current,该对象在renderWithHooks函数(在render阶段被beginWork函数调用)内进行赋值,如下代码,这里的current对象为UI中正在渲染的Fiber,本文称为旧Fiber 。如果旧Fiber不存在(首次渲染),或者旧Fiber的memoizedState为空(首次渲染该组件),则将dispatcher设为挂载阶段对象HooksDispatcherOnMount。
注意,属性
memoizedState保存该Fiber内存储的Hooks链表,这里和Hook对象的memoizedState不一样,Hook对象的memoizedState表示上一次更新后的最终状态值。
js
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
接着调用不同阶段下该dispatcher下的Hooks方法。其内部代码如下所示:
js
// 挂载阶段不同Hook使用的方法
const HooksDispatcherOnMount = {
...
useState: mountState,
};
// 更新阶段不同Hook使用的方法
const HooksDispatcherOnUpdate = {
...
useState: updateState,
};
由于我们只考虑useState的内部实现,所以这里来看mountState和updateState这两个方法。
挂载阶段
在挂载阶段,从创建Hook对象到创建状态更新队列queue,通过bind函数预置参数返回一个新的函数,最终返回一个数组,也就是我们平常开发中结构后的state和setState,分别对应本次更新的状态值和更新函数。
其中包括了对Hook链表的处理,通过遍历Hook链表获取其更新前后的状态值,从而在使用React开发时其能够实现状态变化触发重渲染。
js
function mountState(initialState) {
// 返回新创建的Hook对象
const hook = mountWorkInProgressHook();
// 因为是挂载阶段,传参可能为函数
if (typeof initialState === 'function') {
initialState = initialState();
}
// 其中baseState是因优先级问题被跳过的更新时,下一次计算的基础状态起点
hook.memoizedState = hook.baseState = initialState;
// 存储在Hook的队列对象中,其作为状态管理机制的基础
const queue = hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
const dispatch = queue.dispatch = dispatchAction.bind(
null,
currentlyRenderingFiber, // wip Fiber
queue, // 更新队列
);
return [hook.memoizedState, dispatch];
}
在mountState中,对mountWorkInProgressHook返回的Hook对象进行初始化。在函数mountWorkInProgressHook内部,创建了新的Hook对象,接着根据全局对象workInProgressHook来管理work in progress Fiber(本文称作新Fiber)的Hook链表将新建的Hook追加到链表末尾最后返回。
js
function mountWorkInProgressHook() {
// 创建新Hook对象
const hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 这是当前Fiber上的第一个Hook
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 并非第一个Hook,将新Hook追加到链表末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
再看这里对新建Hook对象的属性处理,其中baseState为上一次更新后得到的基础状态值,这里在挂载阶段由于不存在上一次更新所以直接赋值。接着将状态更新队列存储在queue属性中,例如我们在一次执行阶段触发了对应Hook的dispatch函数,可能会执行多次,如下代码所示:
js
const add = () => {
setCount(count + 1)
setCount(prev => prev + 1)
setCount(count + 1)
}
其中basicStateReducer函数参数的逻辑是:如果action是函数,则调用它并传入当前state;否则直接返回action本身。这也正是useState与useReducer的核心区别------useState的reducer是固定的,而useReducer的reducer由开发者自定义。
接着创建状态更新队列中的dispatch函数,其通过dispatchAction函数绑定返回,这里指定thisArg参数为null是因为该函数内部不需要使用到this,最主要还是起到一个预置参数的作用,在返回的dispatch函数被调用时可以直接使用其传递的参数,我们只需要传递剩余的参数即可,这里我们来拆解dispatchAction方法。
js
function dispatchAction(fiber, queue, action) {
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber);
// 创建Update对象
const update = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// 追加更新对象到更新队列末尾
const pending = queue.pending;
if (pending === null) {
// 首次更新,直接创建循环链表
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// 这是渲染阶段产生的更新,需要暂存在一个以更新队列为键的Map中,在渲染阶段结束后再进行暂存更新的处理
// 这里标记该变量
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
} else {
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// 当前更新队列为空,意味着我们可以在进入下一个渲染阶段前计算好下一次的状态值
// 如果状态值没有变化则直接跳过后续处理
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState = queue.lastRenderedState;
const eagerState = lastRenderedReducer(currentState, action);
// 将预先计算出的状态和用于计算该状态的reducer函数存储在更新对象上
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
// 如果在进入渲染阶段时reducer没有改变,便可直接使用预先该计算的状态,无需再次调用reducer
if (is(eagerState, currentState)) {
return;
}
} catch (error) {
...
}
}
}
// 开启调度
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}
这里我留下了核心的代码部分,其中eventTime和lane分别用于时间片调度和优先级调度,最后会传给scheduleUpdateOnFiber进行任务调度,这里我们不做过多关注。总的来说就是创建更新对象并将其追加到更新队列末尾,在渲染阶段产生更新进行标记,但如果预先计算的新状态与当前状态相同(即is(eagerState, currentState)为 true),则会直接返回,跳过后续的调度和重渲染,作为性能优化路径。
更新阶段
在更新阶段我们updateState内部直接返回updateReducer函数的调用结果。
js
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
注意,在更新阶段,updateState的initialState参数不会被使用。因为此时Hook已经存在,状态直接从旧Fiber树中克隆,初始值只在挂载阶段生效。那我们直接来分析updateReducer函数。
js
function updateReducer(reducer, initialArg, init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current = currentHook;
// 上次渲染阶段因优先级不够而跳过的更新队列
let baseQueue = current.baseQueue;
// 本次渲染阶段产生的新更新
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// 存在仍未处理的新更新
// 将环形链表pendingQueue追加到baseQueue末尾
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
// 修改指针指向现在拼接后的末尾
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 接着遍历合并后的基本更新队列
if (baseQueue !== null) {
const first = baseQueue.next;
// 起始状态(旧树的基准状态)
let newState = current.baseState;
// 新的基准状态(跳过更新时使用)
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
// 该更新的优先级不够,需要跳过
if (!isSubsetOfLanes(renderLanes, updateLane)) {
const clone = {
lane: updateLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: null,
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// 更新在队列中剩余的优先级
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
if (newBaseQueueLast !== null) {
const clone = {
lane: NoLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: null,
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
if (update.eagerReducer === reducer) {
// 如果本次更新预先处理过,在dispatchAction函数中已经针对更新队列为空时进行预先计算
newState = update.eagerState;
} else {
// 否则调用reducer计算新状态值
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;
}
hook.memoizedState = newState; // 更新后的新状态
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState; // 更新在队列中保存的缓存状态
}
const dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
在updateWorkInProgressHook函数内部,保证了每次调用时新旧Fiber的Hook都能对应上,并且实现首次更新或者渲染中断时复用Hook对象。
js
// 通过currentlyRenderingFiber初始化currentHook和wipHook或者通过内部变量管理游标移动,
// 确保第 N 次调用 useState,能对应到第 N 个 current Hook 和 wip Hook
function updateWorkInProgressHook() {
// 这个函数使用在更新和渲染阶段更新触发的重渲染阶段。该函数假定存在一个可以从旧Fiber树克隆的Hook,或者存在一个来自上次渲染的workInProgress Hook可作为基础值复用。
let nextCurrentHook;
// 旧树中不存在该Hook,也就是首次更新
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate; // 旧Fiber
// 直接复用旧树Hook
if (current !== null) {
nextCurrentHook = current.memoizedState; // Hook链表给到nextCurrentHook
} else {
// currentHook为空且旧Fiber为空
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
// 最终拿到的nextCurrentHook即为旧Fiber的Hook,通过每次调用函数移动指针指向链表下一个Hook对象
let nextWorkInProgressHook;
// 首次更新时,wipHook为空,需要从当前正在渲染的wipFiber中获取Hook链表
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
// 这里保证每次调用函数移动指针指向链表下一个Hook对象
// 中断恢复时复用
if (nextWorkInProgressHook !== null) {
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
currentHook = nextCurrentHook;
// 根据上一个currentHook克隆新Hook
const newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// 这是列表中第一个Hook
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
添加新Hook到链表末尾
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
总结
useState的底层实现,是作为React函数组件状态管理的基础。其核心通过双缓存Fiber架构 管理Hook对象链表,每个Hook对象的queue对象维护一个环形更新链表,配合优先级机制 ,支持高优先级更新插队、低优先级更新延迟处理,实现可中断的并发渲染。通过eagerState预计算新状态,若与当前状态相同则跳过调度;在updateReducer中复用缓存的eagerState,避免重复计算。
通过理解useState底层原理,我们能够理解React其声明式UI设计思想,而且也更清晰准确地发现和改善日常开发中遇到的一些问题,写出更高效的React代码。
注:本文基于React 18源码进行分析,关键代码都进行了中文注释标记。