概述
useState赋予了Function Component状态管理的能力,可以让你在不编写 class 的情况下使用 state 。其本质上就是一类特殊的函数,它们约定以 use
开头。本文从源码出发,一步一步看看useState是如何实现以及工作的。
基础使用
javascript
function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>]
在组件顶层通过useState申明状态,useState接收一个参数作为该状态的初始值,该参数可以是任意类型的值,也可以是返回初始值的函数,React内部会进行判断然后执行并缓存该结果。返回一个包含当前状态和更新状态回调的数组
javascript
const [state, setState] = useState(initialState)
所有的Hooks都必须在组件顶层或者自定义Hook顶层中使用,不能在条件、循环语句中使用,这是为了避免Hook的位置混乱导致React无法正确找到状态和Hooks的对应而发生错误
源码解析
前面的文章我们提到过,我们使用的Hooks在React18之后在React内部拆分mount、update两个函数,并由dispatcher管理。下面我们将从入口、mount、update这三个方面来介绍useState的源码实现。
入口:文件路径在react/packages/react/src /ReactHooks.js
javascript
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
我们在组件内调用useState申明状态,实际就是执行的这个函数,然后通过dispatcher根据渲染阶段分发执行那个函数。
Mount挂载时
在挂载时,主要调用了mountState
、mountStateImpl
、mountWorkInProgressHook
下面一一讲解。
mountState: 挂载时调用的函数,接收组件内传入的初始值initialState,然后返回当前状态和更新状态的回调
- 调用
mountStateImpl
在当前渲染fiber节点中创建一个hook list用于对所有hook进行管理 - 根据当前渲染fiber和更新队列创建一个dispatch用于更新状态,由于将dispatch绑定到
dispatchSetState
上,所以当我们通过set
函数更新时,实际执行的是dispatchSetState
函数 - 将dispatch挂载到queue更新队列中,便于后续更新时候直接调用dispatch更新
- 返回包含当前状态和更新回调的数组
javascript
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountStateImpl(initialState);
const queue = hook.queue;
// 将dispatch绑定到dispatchSetState,所以当我们通过set函数更新时,实际执行的是dispatchSetState函数
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
hook和queue的数据结构如下:
javascript
hook = {
memoizedState: initialValue, // 保存当前的状态值
baseState: initialValue, // 初始状态,用于批处理
baseQueue: null, // 需要更新的队列
queue: null, // 更新队列,用于存储状态更新
next: null // 链表中的下一个 Hook 节点
};
const queue = {
pending: null, // 指向尚未处理的更新。这些更新将在下一次渲染时被处理。
lanes: NoLanes, // 优先级
dispatch: null, // 用于分发动作(actions)触发状态更新。
lastRenderedReducer: basicStateReducer, // 最后一次渲染时使用的 reducer 函数。对于 useReducer,它是用户定义的 reducer 函数;对于 useState,它是一个内置的基本状态 reducer
lastRenderedState: (initialState: any), // 保存了上一次渲染时 Hook 的状态,以便在下一次渲染时能够对比新旧状态并进行必要的更新。
};
在mountStateImpl函数中:
- 调用
mountWorkInProgressHook
生成hook - 缓存初始值(如果是函数则执行并缓存结果),其中
memoizedState
会在update时计算新值,而baseState
则记录初始值在批处理的时候会使用 - 基于生成的hook初始化更新队列queue
- 返回这个hook链表给到
mountState
继续处理
javascript
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialState = initialStateInitializer();
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
return hook;
}
mountWorkInProgressHook
主要就是创建初始化一个hook链表,并将其挂载到当前渲染的fiber节点中。
javascript
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
至此useState在挂载时的整个流程就完成了,我们在这里小结一下。当我们在组件中使用useState时,其背后流程就是: Function Component -> FIber节点 -> Hooks链表 -> UpdateQueue -> dispatch(updateState)
。每个函数组件都有一个对应的 Fiber 节点,每个 Fiber 节点都有一个 Hook 链表(比如保存组件中的useState、useEffect等所有hook),用于存储该组件中的所有 Hook。而Hook链表中的每个Hook都有一个UpdateQueue更新队列来对状态进行更新,在更新时会依次遍历这个Hooks链表然后执行对应Hook的更新。
fiber、hook、queue关系图:
Update更新时
上面我们介绍了在使用useState初始挂载一个状态时做了什么工作,现在来看看当组件重新渲染时useState是怎么实现的。
javascript
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}
从源码来看,如果使用过redux的同学可能会很熟悉,就和我们理解的一致也是通过dispatch来调用Reducer来进行updateState的。由此能看出useState 是基于 useReducer 实现,通过调用updateReducer来实现state更新
,其中basicStateReducer
是React内部默认的处理状态更新的reducer。
javascript
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}
通过调用updateReducer
将用于状态更新的reducer和初始值以及通过updateWorkInProgressHook
处理后的链表传递给了updateReducerImpl
进行处理。由于updateReducerImpl
中代码比较多而且还涉及到了Scheduler调度中的优先级,所以对于部分跳过的更新逻辑在这里进行了省略,有兴趣的可以去官网查看【React Github】
updateWorkInProgressHook代码如下:该函数主要逻辑就是复用已有的hook并更新workInProgressHook指针指向下一个hook。优先复用当前渲染中的hook即workInProgress树中当前fiber的hook,如果没有则克隆页面显示的current fiber中的hook,如果都没有则通过throw抛出异常
updateWorkInProgressHook函数处理并更新指针到下一个hook是因为当前hook在上一次渲染或挂载过程中已经执行并存储了状态。这是因为React需要保持对hook链表的引用,以便在下一次渲染时可以复用这些hook。删除hook可能会导致状态丢失和链表结构破坏。所以在React的实现中,不会在上一次渲染时删除hook,而是在下一次渲染时更新指针。
javascript
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook; // 下次更新的hook
// nextCurrentHook值为当前渲染中fiber的下一次hook或者复用页面显示的当前fiber的下一个hook
if (currentHook === null) {
// 通过alternate指针切换workInProgress和current fiber树
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
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) {
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
// This is the initial render. This branch is reached when the component
// suspends, resumes, then renders an additional hook.
// Should never be reached because we should switch to the mount dispatcher first.
throw new Error(
'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
);
} else {
// This is an update. We should always have a current hook.
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,
};
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;
}
updateReducerImpl代码如下:
javascript
/**
*
* hook:指向当前 Fiber 节点正在处理的具体 Hook 实例(即 Hook 链表中的一个节点)。
* current:指向当前 Fiber 节点中对应的 Hook 实例的当前状态(即已渲染到页面上的状态)。
* reducer触发active进行state的更新
*/
function updateReducerImpl<S, A>(
hook: Hook,
current: Hook,
reducer: (S, A) => S
): [S, Dispatch<A>] {
// 获取当前指向hook的更新队列,以及绑定reducer更新函数
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
let baseQueue = hook.baseQueue;
// 如果有上次渲染未处理的更新队列
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// 有上次为处理的更新以及本次也有需要处理的更新,则将两个更新队列合并,否则将上次未处理的赋值给更新队列等待本次渲染更新
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 如果本次没有更新队列,则更新memoizedState为baseState
const baseState = hook.baseState;
if (baseQueue === null) {
hook.memoizedState = baseState;
} else {
// 更新队列有状态需要更新
const first = baseQueue.next;
let newState = baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast: Update<S, A> | null = null;
let update = first;
let didReadFromEntangledAsyncAction = false;
do {
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
// 根据优先级判断当前是否需要跳过更新,并保存在newBaseQueueLast中在下次渲染时更新,然后调用markSkippedUpdateLanes跳过本次更新
if (shouldSkipUpdate) {
...
} else {
const revertLane = update.revertLane;
// 根据优先级判断当前是否需要跳过更新,并保存在newBaseQueueLast中在下次渲染时更新
if (!enableAsyncActions || revertLane === NoLane) {
...
} else {
// 将符合本次更新条件的状态保存在update链表中,等待更新
if (isSubsetOfLanes(renderLanes, revertLane)) {
update = update.next;
if (revertLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}
continue;
} else {
// 不符合的保存在newBaseQueueLast等待下次渲染时候更新
...
}
}
// 开始更新,当前update对象是否提前计算,否则通过reducer处理
const action = update.action;
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
// 遍历本次更新队列之后,判断是否有跳过的更新,如果有则保存在newBaseState中,等待下次渲染时更新
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// 判断上一次的状态和reducer更新之后的状态是否一致,发生变化则通过markWorkInProgressReceivedUpdate函数给当前fiber打上update标签
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
// 检查异步处理操作,如果说是异步获取值,则需要通过throw entangledActionThenable将当前更新挂起
if (didReadFromEntangledAsyncAction) {
const entangledActionThenable = peekEntangledActionThenable();
if (entangledActionThenable !== null) {
throw entangledActionThenable;
}
}
}
// 将本次新的state保存在memoizedState中
hook.memoizedState = newState;
// 保存下次更新的初始值,如果本次没有跳过更新,该值为更新后通过reducer或者eagerState计算的新值,有跳过的更新则会本次更新前原来的初始值
hook.baseState = newBaseState;
// 将本次跳过的更新保存在baseQueue更新队列中中,下次渲染时更新
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
// 没有状态更新时,将当前队列优先级设置为默认
if (baseQueue === null) {
queue.lanes = NoLanes;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
根据上面代码以及注释可知,在updateReducerImpl主要做了一下功能:
- 获取和合并更新队列。
- 遍历更新队列,根据优先级判断是否需要跳过,计算新的更新队列
- 通过reducer进行状态更新
- 更新基础状态和队列
- 处理异步操作的影响
- 返回新的状态和 dispatch 函数
useState于update的差异
根据下面表格可以简要总结两者差异主要是:mountState进行初始化挂载,updateState在其基础上进行更新队列处理,包括跳过低优先级任务以及提前处理任务。
比较点 | mountState | updateState |
---|---|---|
创建 Hook 和更新队列 | 是 | 是 |
返回值 | 返回包含当前状态值和 set 函数的数组 | 返回包含当前状态值和 set 函数的数组 |
用途 | 主要用于组件的首次渲染,初始化数据挂载 | 处理组件的更新渲染,包含对上次渲染跳过的部分进行处理 |
初始状态处理 | 计算并设置初始状态(如果 initialState 是函数,会调用它) |
不处理初始状态,只处理更新 |
更新队列 | 创建新的更新队列,并将其分配给 Hook | 处理现有更新队列,可能需要合并新的和已有的更新队列 |
状态计算 | 直接设置初始状态 | 通过调用 reducer (通常是 basicStateReducer )计算新状态 |
Hook 链表 | 将 Hook 添加到当前 Fiber 的 Hook 链表中 | 更新当前 Fiber 的 Hook 链表中的现有 Hook |
优先级处理 | 不涉及优先级处理 | 可能需要处理优先级和跳过的更新 |
set
函数
由上面知道了,使用useSatte会返回包含[newState,setValue]
的数组,然后我们调用setValue可以更新状态值,而这个dispatch实际是执行的dispatchSetState
(在mountState中通过bind进行了绑定),所以下面来看看在dispatchSetState
函数中做了什么?
javascript
// 触发更新后,React会判断当前是否还有其他渲染或者挂起,没有会提前计算值,不变则跳过更新
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
// 获取优先级
const lane = requestUpdateLane(fiber);
// 创建一个update更新对象
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
// 当前是否是渲染阶段
if (isRenderPhaseUpdate(fiber)) {
// 申请加入正在渲染的更新队列
enqueueRenderPhaseUpdate(queue, update);
} else {
// 页面上显示的fiber树 - 双缓冲树,通过alternate切换指针
const alternate = fiber.alternate;
// 判断current树和workInProgress树是否有挂起的更新,如果没有则进入提前计算逻辑
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher = null;
try {
const currentState: S = (queue.lastRenderedState: any);
// 用于update state的reducer,由React内部提供,所以这里使用缓存的lastRenderedReducer可以提高性能
const eagerState = lastRenderedReducer(currentState, action);
// 标识该值已经提前计算
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// 无更新则跳过更新步骤
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
}
}
}
// 进入组件更新步骤 enqueueConcurrentHookUpdate会将update更新对象添加到enqueueUpdate更新队列中
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
// 进入Scheduler调度阶段,进行fiber构造
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
}
}
}
从代码中能知道在函数中主要:
- 创建一个带有优先级的update对象
- 判断当前是否是渲染更新阶段,如果是则直接将update对象添加到更新队列中等待更新,否则判断
current
树和workInProgress
树是否有挂起的更新,如果没有则进入提前计算并判断值是否变化,没有变化则跳过更新,否则就进入组件更新步骤将update对象添加到enqueueUpdats
更新队列中然后调用scheduleUpdateOnFiber
等待调度更新
其中enqueueConcurrentHookUpdate
函数主要就是将创建的update对象添加到当前fiber的更新队列enqueueUpdate中,其中通过Map
、Set
来对渲染更新数据进行管理
javascript
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
) {
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
let firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
// Append the update to the end of the list.
let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
}
将本次更新的update对象添加到更新队列中,会调用scheduleUpdateOnFiber
等待调度更新然后进行新的fiber树构造。
这里从宏观角度,简单说下React多个包之间的流程来帮助我们理解。 React中有几个核心包:react、react-dom、react-reconciler(协调)、scheduler(调度)
,其中上面的update更新对象处理以及将其添加到更新队列中是在react
包中处理的,然后调用scheduler
包中的scheduleUpdateOnFiber
等待调度进入react-reconciler
包中处理进行renderer阶段进行fiber构造
,最后进入react-dom
进行commit阶段
进行页面的渲染。
上图就是React总的流程图,各个核心流程都在里面,有兴趣的同学可以查看我写的其他React源码系列,比如【React架构 - Fiber构造循环】
总结
上面说了这么多这里进行简单总结一下,有的点可能会多次提及为了巩固记忆。
正文开始~~
在页面渲染过程中有两个阶段分别为mount(首次渲染)、update(更新渲染),而React为了更好的管理和优化副作用将Hooks(useContext除外)拆为了mount、update两个函数。通过内置的dispatcher管理,React会根据目前具体处于什么阶段来决定调用那个函数,比如在mount阶段,会调用mountState函数,这对于开发者来说是无感的,React在内部进行了映射。具体的流程走向如下图所示
mount、update时,state的处理:
当触发set
函数进行状态更新时:
以上都是根据自己理解进行总结梳理的,如果理解有误还请评论指正。