前言
useState在React中无时无刻不在使用的一个hooks,有时候一些奇怪的问题和现象都会让使用者摸不着头脑,因此,深入理解源码,才能明白和避免问题的产生。
useState引入后会发生什么
tsx
import { Button } from "antd";
import { useState } from "react";
const Index = () => {
const [count, setCount] = useState(0);
return (
<>
<div>数字:{count}</div>
<Button onClick={() => setCount((v) => v + 1)}>点击加1</Button>
</>
);
};
export default Index;
这是一个简单的示例,当我们引入了useState,并存储了count变量,通过setCount来控制count的值,count是这个函数式组件的状态,setCount就是触发数据更新的函数。
接下来,我们看看源码,调用useState后,会执行那些逻辑。
文件位置:packages/react/src/ReactHooks.js
。
ts
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 重点在这个resolveDispatcher
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
可以看出 useState 的执行就等价于 resolveDispatcher().useState(initialState)
文件位置:packages/react/src/ReactCurrentDispatcher.js
。
ts
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
通过类型可以看到 ReactCurrentDispatcher
不是 null,就是 Dispatcher,而在初始化时 ReactCurrentDispatcher.current
的值必为 null,因为此时还未进行操作。
那么此时就很奇怪了,我们并没有发现 useState 是如何进行存储、更新的,ReactCurrentDispatcher.current
又是何时为 Dispatcher
的?
既然我们在 useState 自身中无法看到存储的变量,那么就只能从函数执行开始,一步一步探索 useState 是如何保存数据的。
函数式组件执行流程
JSX代码最终会走到beginWork这个方法中,而beginWork会走到mountIndeterminateComponent中,这个方法中会有一个函数叫renderWithHooks。
renderWithHooks 就是所有函数式组件触发函数
文件位置:packages/react-reconciler/src/ReactFiberHooks
。
tsx
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
currentlyRenderingFiber = workInProgress;
// memoizedState: 用于存放hooks的信息,如果是类组件,则存放state信息
workInProgress.memoizedState = null;
//updateQueue:更新队列,用于存放effect list,也就是useEffect产生副作用形成的链表
workInProgress.updateQueue = null;
// 用于判断走初始化流程还是更新流程
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 执行真正的函数式组件,所有的hooks依次执行
let children = Component(props, secondArg);
finishRenderingHooks(current, workInProgress);
return children;
}
function finishRenderingHooks(current: Fiber | null, workInProgress: Fiber) {
// 防止hooks乱用,所报错的方案
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
// current树
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
}
const HooksDispatcherOnMount: Dispatcher = {
readContext,
use,
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,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
useHostTransitionStatus: useHostTransitionStatus,
useFormState: mountActionState,
useActionState: mountActionState,
useOptimistic: mountOptimistic,
useMemoCache,
useCacheRefresh: mountRefresh,
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
use,
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,
useSyncExternalStore: updateSyncExternalStore,
useId: updateId,
useHostTransitionStatus: useHostTransitionStatus,
useFormState: updateActionState,
useActionState: updateActionState,
useOptimistic: updateOptimistic,
useMemoCache,
useCacheRefresh: updateRefresh,
};
上述代码块忽略了DEV代码,我们先分析下 renderWithHooks 函数的入参。
- current: 即 current fiber,渲染完成时所生成的 current 树,之后在 commit 阶段替换为真正的 DOM 树;
- workInProgress: 即 workInProgress fiber,当更新时,复制 current fiber,从这棵树进行更新,更新完毕后,再赋值给 current 树;
- Component: 函数组件本身;
- props: 函数组件自身的 props;
- secondArg: 上下文;
- nextRenderLanes: 渲染的优先级。
可以看出初始化阶段和更新阶段调用的逻辑是不一样的,每个hooks在每个阶段都有对应的逻辑函数。
小知识:Fiber架构的三个阶段分别为reconcile、schedule、commit 阶段。
- reconcile 阶段: vdom 转化为 fiber 的过程。
- schedule 阶段:在 fiber 中遍历的过程中,可以打断,也能再恢复的过程。
- commit 阶段:fiber 更新到真实 DOM 的过程。
renderWithHooks的执行过程
- 在每次函数组件执行之前,先将 workInProgress 的 memoizedState 和 updateQueue 属性进行清空,之后将新的 Hooks 信息挂载到这两个属性上,之后在 commit 阶段替换 current 树,也就是说 current 树保存 Hooks 信息;
- 然后通过判断 current 树是否存在来判断走初始化( HooksDispatcherOnMount )流程还是更新( HooksDispatcherOnUpdate )流程。而 ReactCurrentDispatcher.current 实际上包含所有的 Hooks,简单地讲,React 根据 current 的不同来判断对应的 Hooks,从而监控 Hooks 的调用情况;
- 接下来调用的 Component(props, secondArg) 就是真正的函数组件,然后依次执行里面的 Hooks;
- 最后提供整个的异常处理,防止不必要的报错,再将一些属性置空,如:currentHook、workInProgressHook 等。
# HooksDispatcherOnMount(初始化阶段)
在初始化阶段中,调用的是 HooksDispatcherOnMount
,对应的 useState 所走的是 mountState
。
ts
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = (initialState);
const queue = hook.queue;
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
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;
}
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;
}
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
const lane = requestUpdateLane(fiber);
const didScheduleUpdate = dispatchSetStateInternal(
fiber,
queue,
action,
lane,
);
if (didScheduleUpdate) {
startUpdateTimerByLane(lane, 'setState()', fiber);
}
}
通过mountState可以看到,通过mountStateImpl方法里的mountWorkInProgressHook()方法拿到hooks,它的作用尤为重要,因为这个函数的作用是将 Hooks 与 Fiber 联系起来 ,并且你会发现,所有的 Hooks 都会走这个函数,只是不同的 Hooks 保存着不同的信息。
mountWorkInProgressHook()方法会判断当前的Fiber是否有workInProgressHook,如果没有的,则创建,有的话,生成一个hook并赋值给workInprogressHook.next,并 workInProgressHook = workInProgressHook.next。最后返回workInprogressHook。
来看看 hook 值的参数:
- memoizedState:用于保存数据,不同的 Hooks 保存的信息不同,比如 useState 保存 state 信息,useEffect 保存 effect 对象,useRef 保存 ref 对象;
- baseState:当数据发生改变时,保存最新的值;
- baseQueue:保存最新的更新队列;
- queue:保存待更新的队列或更新的函数;
- next:用于指向下一个 hook 对象。
那么 mountWorkInProgressHook
的作用就很明确了,每执行一个 Hooks 函数就会生成一个 hook 对象,然后将每个 hook 串联起来。
特别注意:这里的 memoizedState 并不是 Fiber 链表上的 memoizedState,workInProgress 保存的是当前函数组件每个 Hooks 形成的链表。
再来看看dispatchSetState
dispatch 的机制就是 dispatchSetState
,通过观察dispatchSetState发现,其实内部就是调用了dispatchSetStateInternal方法,我们来看看dispatchSetStateInternal方法
ts
function dispatchSetStateInternal<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
lane: Lane,
): boolean {
const update: Update<S, A> = {
lane,
revertLane: NoLane,
gesture: null,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
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);
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return false;
}
} catch (error) {
} finally {
}
}
}
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
return true;
}
}
return false;
}
首先会生成一个update对象,用于记录更新信息,其次会 判断是否处于渲染阶段 :如果是渲染阶段,则将 update
放入等待更新的 pending
队列中,如果不是,就会获取最新的 state
值,从而进行更新。
值得注意的是:在更新过程中,也会判断很多,通过调用 lastRenderedReducer
获取最新的 state
,然后进行比较(浅比较) ,如果相等则退出,这一点就是证明 useState 渲染相同值时,组件不更新的原因。
如果不相等,则会将 update
插入链表的尾部,返回对应的 root
节点,通过 scheduleUpdateOnFiber 实现对应的更新 ,可见 scheduleUpdateOnFiber
是 React 渲染更新的主要函数。
HooksDispatcherOnUpdate(更新阶段)
在更新阶段时,调用 HooksDispatcherOnUpdate
,对应的 useState
所走的是 updateState
文件位置:packages/react-reconciler/src/ReactFiberHooks.js
。
ts
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
basicStateReducer
很简单,判断是否是函数,返回对应的值即可。
那么下面主要看 updateReducer
这个函数,在 updateReducer 函数中首先调用 updateWorkInProgressHook
,我们先来看看这个函数,方便后续对 updateReducer 的理解。
updateWorkInProgressHook
跟 mountWorkInProgressHook
一样,当函数更新时,所有的 Hooks 都会执行。
文件位置:packages/react-reconciler/src/ReactFiberHooks.js
。
ts
function updateWorkInProgressHook(): Hook {
// 初始化 `nextCurrentHook`(对应上一次渲染的 Hook)
let nextCurrentHook: null | Hook;
// 第一个hook渲染 currentHook为空 就说明是第一个hook
if (currentHook === null) {
// 对应的"旧" Fiber 节点(上次渲染的结果)
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
// 拿到上一个旧Fiber数的第一个hook
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
}
else {
// 不是第一个hook 通过hooks链表拿到下一个hook
nextCurrentHook = currentHook.next;
}
// 获取当前 WIP(Work In Progress)中的Hook
let nextWorkInProgressHook: null | Hook;
// workInProgressHook为null 就说明还在构建新的hook链表
if (workInProgressHook === null) {
// `currentlyRenderingFiber`:当前正在渲染的 Fiber 节点(即 `WIP`, Work-In-Progress)
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
//
if (nextCurrentHook === null) {
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
throw new Error(
'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
);
} else {
}
}
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
updateWorkInProgressHook 执行流程:首先需要拿到旧Fiber树的Hook(nextCurrentHook、currentHook),这两个变量都是在操作旧Fiber树上的Hook,(workInProgressHook、nextWorkInProgressHook),这两个变量是操作当前Fiber树上的Hook,如果是第一次执行updateWorkInProgressHook方法,此时的currentHook即为空,会拿到上一个Fiber树的hook,如果workInProgressHook为null,就说明当前的workInProgressHook正在构建,nextWorkInProgressHook = currentlyRenderingFiber.memoizedState这块nextWorkInProgressHook会为null,所以会重新构建hook走到else的逻辑中。
掌握了 updateWorkInProgressHook
执行流程后, 再来看 updateReducer
具体有哪些内容。
ts
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 获取更新的hook,每个hook都会走
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
let baseQueue = current.baseQueue;
// 在更新的过程中,存在新的更新,加入新的更新队列
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// 如果在更新过程中有新的更新,则加入新的队列,有个合并的作用,合并到 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 = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
if (shouldSkipUpdate) {
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;
}
// 合并优先级(低级任务)
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
// 判断更新队列是否还有更新任务
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
// 将更新任务插到末尾
newBaseQueueLast = newBaseQueueLast.next = clone;
}
const action = update.action;
// 判断更新的数据是否相等
if (update.hasEagerState) {
newState = ((update.eagerState: any): S);
} else {
newState = reducer(newState, action);
}
}
// 判断是否还需要更新
update = update.next;
} while (update !== null && update !== first);
// 如果 newBaseQueueLast 为null,则说明所有的update处理完成,对baseState进行更新
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// 如果新值与旧值不想等,则触发更新流程
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 将新值,保存在hook中
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
if (baseQueue === null) {
queue.lanes = NoLanes;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
updateReducer 的作用是将待更新的队列 pendingQueue 合并到 baseQueue 上,之后进行循环更新,最后进行一次合成更新,也就是批量更新,统一更换节点。
这种行为解释了 useState 在更新的过程中为何传入相同的值,不进行更新,同时多次操作,只会执行最后一次更新的原因了。