在上一篇文章中讲到 Hooks 链表是 React 内部用于在函数组件中管理多个 Hooks 状态的数据结构。它是在 Render 阶段创建的,用于记录函数组件中所有使用的 Hooks 及其对应的状态信息。通过 Hooks 链表,React 能够准确地跟踪每个 Hook 的调用顺序和状态,从而实现组件的状态管理和更新。从而实现更高效、可预测的组件渲染和更新。
那么这篇文章我们就来学习一下 useState 这个 hook 的具体实现。
useState 原理
对于 useState,我们可以把工作流程分为 声明阶段
和 调用阶段
,对于:
- 声明阶段即 App 调用时,会依次执行 useReducer 与 useState 方法;
- 调用阶段即点击按钮后,setState 被调用时;
它们具体在源码中表现为以下数据结构,首先是声明阶段的:
ts
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,
};
紧接着是调用阶段的:
js
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,
};
声明阶段
mount 时 useState 会调用 mountState,代码如下图所示:
mountState
它主要是用于在组件的初始渲染阶段 mounting 阶段
创建和管理状态的。让我们逐步解释这个函数的主要步骤:
-
调用
mountWorkInProgressHook()
获取当前组件正在工作中的Hook
,也就是开始构造 hooks 链表了: -
如果传入的 initialState 是一个函数,则执行该函数,获取初始状态值。这是因为 initialState 参数可以接收一个返回初始状态的函数,这样可以实现状态的惰性初始化;
-
将初始状态值赋给 hook.memoizedState 和 hook.baseState。hook.memoizedState 是 Hook 实例中用于存储状态的字段,而 hook.baseState 是用于保存初始状态值的字段:
-
hook.memoizedState:它是用来存储组件当前状态的对象。每当组件重新渲染时,memoizedState 将会更新以反映最新的状态值。该对象包含一个 value 属性,表示当前的状态值;
-
hook.baseState:它是用来存储组件初始状态的对象。它只在组件初次渲染时被创建,并且在后续的重新渲染中不会更新。baseState 的值在组件生命周期内保持不变,可以看作是状态的初始值;
如果 initialState 是函数,会将函数的返回值重新赋值给 initialState。
-
-
创建一个 queue 对象用于保存状态的更新。这个对象包含了以下字段:
- pending: 一个链表结构,用于保存待处理的状态更新;
- interleaved: 用于处理优先级较低的状态更新。当并发模式
Concurrent Mode
下有多个状态更新时,React 为了保证性能,可能会将新的较低优先级的状态更新插入到已有的更新之间,形成一个交错 interleaved 的更新链; - lanes: 用于调度和跟踪状态更新的优先级;
- dispatch: 用于触发状态更新的 dispatch 函数;
- lastRenderedReducer: 最近一次渲染时使用的状态更新函数,它绑定了一个函数 basicStateReducer,该函数具体代码如下所示:
jsfunction basicStateReducer<S>(state: S, action: BasicStateAction<S>): S { console.log("🚀 ~ file: ReactFiberHooks.old.js:701 ~ action:", action); console.log("🚀 ~ file: ReactFiberHooks.old.js:701 ~ state:", state); return typeof action === "function" ? action(state) : action; }
在上面的代码中我们分别打印两个参数,并在我们的应用程序中添加以下代码:
jsximport React, { useState } from "react"; const App = () => { const [count, setCount] = useState(0); const handleClick = () => { setCount((a) => { console.log(a); }); }; return ( <div> <p>砍几刀: {count}</p> <button onClick={handleClick}>是兄弟就来砍我</button> </div> ); }; export default App;
最终当我们点击该按钮的时候,会有以下输出:
再看一张图片:
也就是说,这个函数主要做的事情是更新时用到的,如果传进来的是函数,传进来 hook.memoizedState 的值调用后返回。如果是普通的值,也就是 action 的值为一个普通的值,则直接返回。
- lastRenderedState: 最近一次渲染时的状态值;
-
将创建的 queue 对象赋值给 hook.queue,将 dispatchSetState 函数绑定到 queue.dispatch。dispatchSetState 是用于触发状态更新的内部函数,它会被传递给组件中的
setState
方法,并在调用setState
时执行状态更新逻辑;
dispatchSetState 记住这个函数,后面会讲到,这也是更新的关键。
mountState
函数在组件挂载阶段起到了初始化和设置组件初始状态的作用。它创建了状态更新队列,并将初始状态设置为组件的当前状态。这些操作对于组件的首次渲染非常重要,后续的状态更新也会依赖于这个初始状态。
这个函数负责处理组件状态的更新,并根据不同的情况进行处理。在渲染阶段外,它会尝试提前计算状态,以便在渲染阶段进一步优化性能。然后,它会将更新添加到更新队列中,并通过 Scheduler 机制安排 React 在合适的时间进行渲染。
创建更新队列
调用阶段会执行 dispatchSetState,这是由 React 内部向开发者暴露出来的 useState 或者 useReducer 的内部实现中调用的其中一个函数,例如,在 React 中有如下代码:
js
const [count, setCount] = useState(0);
其中 setCount 就是 dispatchSetState:
这正是我们在 mountState 给我们 return 回来的第二个参数,该函数代码如下所示:
js
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A
) {
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
// ...
}
首先获取一个更新优先级的 lane,用于确定更新优先级并创建一个 update 对象,包含了状态更新的相关信息:优先级 lane、更新操作 lane、是否有预处理状态的标准、预处理的值、以及下一个更新对象的指针。
接下来我们看看后半部分代码是在干什么:
首先会检查 Fiber 树的 alternate 是否存在且其 lanes 也为 NoLanes,即组件的备份没有正在处理的更新。如果满足以上两个条件,说明组件的状态更新队列是空的,此时可以提前计算新的状态称为 eagerState
而不用等待到渲染阶段。这是一种优化措施,避免不必要的渲染。
在这里我们 lastRenderedReducer 就是我们之前所讲到的函数,用于获取最新传进来的 action,也就是我们使用 setState 中传进来的参数,又因为 currentState 是一个数,所以最终返回 1。也就是 eagerState 是 1。
紧接着对 eagerState 和 currentState 两个值进行比较,如果相等的话直接返回。如下代码所示:
如果调用 setState 传入的值和初始化的值或者上一次更新的值一样就不会发生变化。
然后代码走到这里:
js
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
如果 enqueueConcurrentHookUpdate 函数返回一个根 Fiber 节点 root,说明组件的状态更新队列发生了变化。这时,调用 scheduleUpdateOnFiber 函数触发组件的重新渲染,将根 Fiber 节点、当前 Fiber 节点、更新的优先级和事件时间传递给 scheduleUpdateOnFiber 进行调度。
enqueueConcurrentHookUpdate
这个 enqueueConcurrentHookUpdate 函数也很关键,它不仅会返回一个根 fiber 节点,而且它内部会构造一个 update 循环链表,也就是我们前面中说到的,具体代码如下所示:
ts
export function enqueueConcurrentHookUpdate<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
lane: Lane
) {
const interleaved = queue.interleaved;
console.log(update);
if (interleaved === null) {
update.next = update;
pushConcurrentUpdateQueue(queue);
} else {
update.next = interleaved.next;
interleaved.next = update;
}
queue.interleaved = update;
return markUpdateLaneFromFiberToRoot(fiber, lane);
}
enqueueConcurrentHookUpdate 函数是 React 在并发模式下用于将 Hook 状态更新添加到队列的关键函数。它将状态更新 update 插入到组件的状态更新队列中,通过循环链表的形式确保更新的顺序。同时,通过 markUpdateLaneFromFiberToRoot 函数,将更新的优先级从当前 Fiber 节点传递到根 Fiber 节点,以保证更新的优先级能够正确决定更新的处理顺序。这是 React 并发模式下状态更新和渲染的关键部分。
接下来我们来看看这个循环链表的结构是怎么样的:
所以最终形成的链表也应该是这样的:
整个环形链表变量我们叫它 update,使得 queue.pending = update 那么此时 queue.pending 的最近一次更新,就是 update,最早的一次更新是 update.next。这样就快速定位到最早的一次更新了。
调用阶段
来到了最后一个步骤,也就是 update 时,useReducer 与 useState 调用的都是同一个函数:
ts
function updateState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
在这里我们直接上一个完整的 updateReducer 的源码,emm 这个函数的代码还挺多的,如下:
js
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S
): [S, Dispatch<A>] {
debugger;
const hook = updateWorkInProgressHook();
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 = reducer;
const current: Hook = (currentHook: any);
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
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.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
if (__DEV__) {
if (current.baseQueue !== baseQueue) {
// Internal invariant that should never happen, but feasibly could in
// the future if we implement resuming, or some form of that.
console.error(
"Internal error: Expected work-in-progress queue to be a clone. " +
"This is a bug in React."
);
}
}
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;
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),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
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) {
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
newState = ((update.eagerState: any): S);
} else {
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();
}
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];
}
接下来让我们逐步解释一下它的功能和主要作用:
-
获取当前组件的钩子对象:使用 updateWorkInProgressHook() 函数获取当前正在更新的组件的 hook 对象,也就是我们之前说到的 hooks 链表;
-
更新 lastRenderedReducer:将当前组件的更新队列的 lastRenderedReducer 字段设置为传入的 reducer 函数,用于标记当前使用的 reducer;
reducer 就是我们在项目中使用 useReducer 的 hook 中要传入第一个函数,而在 useState 中也是要传入参数的,只不过是 useState 内部已经帮我们做了,所以它永远是一个函数,用于根据操作类型来更新状态的逻辑。
-
合并新的更新到基础队列 baseQueue:
-
获取当前组件的备用
current
hooks 链表; -
获取当前 hooks 链表的基础队列
baseQueue
和待处理队列pendingQueue
; -
如果有待处理队列,则将其合并到基础队列中;
-
如果 current.baseQueue 和 baseQueue 不相等,表示在合并更新队列的过程中出现了意外情况,这被认为是一个内部错误,因为 current.baseQueue 应该是 baseQueue 的一个克隆,它们应该是相等的。
-
将 baseQueue 更新为合并后的队列,并将 queue.pending 设置为 null,表示待处理队列已经被合并到基础队列中;
这里也就是我们常说的合并更新、批处理,这种行为解释了 useState 在更新的过程中为何传入相同的值,不进行更新,同时多次操作,只会执行最后一次更新的原因了。
如下图所示,我就点击了一次,无论你前面的值是多大,它只会计算最后一次:
-
-
处理基础队列中的更新: 如果基础队列不为空,则遍历基础队列中的更新,如果优先级足够高,则处理该更新:
- 如果该更新是提前计算的状态 hasEagerState 为 true,则直接使用该状态;
- 否则,使用 reducer 函数来计算新的状态值;
-
更新钩子对象的状态:
- 将计算得到的新状态值 newState 设置为钩子对象的 memoizedState,表示当前组件的状态;
- 将新的基础队列 newBaseQueueLast 设置为钩子对象的 baseQueue,用于下次渲染时处理;
-
返回状态和状态更新的 dispatch 函数:返回组件的当前状态值 hook.memoizedState 和更新状态的 dispatch 函数 queue.dispatch,供组件使用;
这个函数的目的是将待处理的状态更新合并到基础队列中,实现状态更新的批处理。通过批处理,React 可以将多个连续的状态更新合并为一个,从而优化性能。如果在连续的渲染过程中有多个状态更新,React 会将它们合并为一个较大的更新,避免不必要的重复渲染和效率损失。
最后我们看一下这段代码:
最终效果如下图所示:
dispatchSetState 函数被打印了四次且是最先执行的,而 updateState 被调用了一次,而且是最后一次执行的,这个也是最后执行的,并且会合并更新。
最终总结一下整个完整的流程,如下图所示:
参考文章
总结
说了这么久,我现在就来把这些小秘密告诉你:
- useState() 初始化参数传入函数作为初始状态可以在很大程度上优化组件的性能,避免不必要的计算和渲染开销,特别是对于复杂的初始化逻辑或嵌套组件来说,效果更为显著。在组件中使用传入函数作为初始状态,是一种非常有用的优化技巧;
- 在调用 setState 去更新状态的时候,React 内部会进行类似 Object.is() 这样的浅比较,如果值相等则不会重新渲染;
- setState 除了能传递一个值外,还可以传入回调函数,回调函数的参数就是最新的 state 的值;
- 如果同时调用多次相同的 setState,最终会被合并成一次,值是最后一次调用的那个;
- useState 是 useReducer 的破解版;
- 我是靓仔;
useState 通过 Hook 实例来管理组件的状态,使用更新队列来收集和处理状态更新,利用渲染阶段来计算最终的状态值,并实现状态的合并和优化。这种实现方式使得组件状态的管理更加高效和灵活,同时也确保了组件状态的正确性和一致性。
最后分享两个我的两个开源项目,它们分别是:
这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🥰🥰🥰