React 知命境第 38 篇,原创第 148 篇
在前面的一篇文章中,我们介绍了 Fiber 的详细属性所代表的含义。在函数式组件中,其中与 hook
相关的属性为 memoizedState
Fiber = {
memoizedState: Hook
}
Fiber.memoizedState 是一个链表的起点,该链表的节点信息为
export type Hook = {
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: any,
next: Hook | null,
}
useState 调用分为两个阶段,一个是初始化阶段,一个是更新阶段。当我们在 beginWork
中调用 renderWithHooks
时,通过判断 Fiber.memozedState
是否有值来分辨当前执行属于初始阶段还是更新阶段。
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
在 react 模块中,我们可以看到 useState 的源码非常简单
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
这里的 dispatcher,其实就是我们在 react-reconciler
中判断好的 ReactCurrentDispatcher.current
hook 的初始化方法挂载在 HooksDispatcherOnMount
上
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,
};
hook 的更新方法挂载在 HooksDispatcherOnUpdate
上
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,
};
因此,在初始化时,useState 调用的是 mountState
,在更新时,useState 调用的是 updateState
0
mountState
mountState 的源码如下
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
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;
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
理解这个源码的关键在第一行代码
const hook = mountWorkInProgressHook();
react 在 ReactFiberHooks.new.js
模块全局中创建了如下三个变量
let currentlyRenderingFiber: Fiber = (null: any);
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
currentlyRenderingFiber
表示当前正在 render 中的 Fiber 节点。currentHook
表示当前 Fiber 的链表。
workInProgressHook
表示当前正在构建中的新链表。
mountWorkInProgressHook
方法会创建当前这个 mountState
执行所产生的 hook 链表节点。
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 作为第一个节点
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 添加到链表的下一个节点
workInProgressHook = workInProgressHook.next = hook;
}
// 返回当前节点
return workInProgressHook;
}
hook 节点的 queue 表示一个新的链表结构,用于存储针对同一个 state 的多次 update
操作。,.pending
指向下一个 update
链表节点。此时因为是初始化操作,因此值为 null
,此时我们会先创建一个 queue
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
此时,dispatch 还没有赋值。在接下来我们调用了 dispatchSetState
,我们待会儿来详细介绍这个方法,他会帮助 queue.pending
完善链表结构或者进入调度阶段,并返回了当前 hook 需要的 dispatch
方法
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
最后将初始化之后的缓存值和操作方法通过数组的方式返回。
return [hook.memoizedState, dispatch];
1
updateState
更新时,将会调用 updateState
方法,他的代码非常简单,就是直接调用了一下 updateReducer
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
这里的需要注意的是有一个模块中的全局方法 basicStateReducer
,该方法执行会结合传入的 action
返回最新的 state 值。
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
代码中区分的情况是 useState
与 useReducer
的不同。useState 传入的是值,而 useReducer
传入的是函数
2
updateReducer
updateReducer
的代码量稍微多了一些,不过他的主要逻辑是计算出最新的 state
值。
当我们使用 setState
多次调用 dispatch
之后, 在 Hook 节点的 hook.queue
上会保存一个循环链表用于存储上一次的每次调用传入的 state 值,updateReducer 的主要逻辑就是遍历该循环链表,并计算出最新值。
此时首先会将 queue.pending
的链表赋值给 hook.baseQueue
,然后置空 queue.pending
const pendingQueue = queue.pending;
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
然后通过 while
循环遍历 hook.baseQueue
通过 reducer 计算出最新的 state
值
// 简化版代码
const first = baseQueue.next;
if (first !== null) {
let newState = current.baseState;
let update = first;
do {
// 执行每一次更新,去更新状态
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
}
最后再返回
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
3
dispatchSetState
当我们调用 setState 时,最终调用的是 dispatchSetState
方法
setLoading -> dispatch -> dispatchSetState
该方法有两个逻辑,一个是同步调用,一个是并发模式下的异步调用。
同步调用时,主要的目的在于创建 hook.queue.pending
指向的环形链表。
首先我们要创建一个链表节点,该节点我们称之为 update
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
然后会判断是否在 render 的时候调用了该方法。
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
isRenderPhaseUpdate
用于判断当前是否是在 render 时调用,他的逻辑也非常简单
function isRenderPhaseUpdate(fiber: Fiber) {
const alternate = fiber.alternate;
return (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
);
}
这里需要重点关注是 enqueueRenderPhaseUpdate
是如何创建环形链表的。他的代码如下
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
) {
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
const pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
我们用图示来表达一下这个逻辑,光看代码可能理解起来比较困难。
当只有一个 update 节点时

新增一个

再新增一个

在后续的逻辑中,会面临的一种情况是当渲染正在发生时,收到了来自并发事件的更新,我们需要等待直到当前渲染结束或中断再将其加入到 Fiber/Hook
队列。因此React 需要一个数组来存储这些更新,代码逻辑如下
const concurrentQueues: Array<any> = [];
let concurrentQueuesIndex = 0;
function enqueueUpdate(
fiber: Fiber,
queue: ConcurrentQueue | null,
update: ConcurrentUpdate | null,
lane: Lane,
) {
concurrentQueues[concurrentQueuesIndex++] = fiber;
concurrentQueues[concurrentQueuesIndex++] = queue;
concurrentQueues[concurrentQueuesIndex++] = update;
concurrentQueues[concurrentQueuesIndex++] = lane;
concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);
fiber.lanes = mergeLanes(fiber.lanes, lane);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
}
在这个基础之上,React 就有机会处理那些不会立即导致重新渲染的更新进入队列。如果后续有更高优先级的更新出现,将会重新对其进行排序
export function enqueueConcurrentHookUpdateAndEagerlyBailout<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
): void {
// This function is used to queue an update that doesn't need a rerender. The
// only reason we queue it is in case there's a subsequent higher priority
// update that causes it to be rebased.
const lane = NoLane;
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
}
dispatchSetState
的逻辑中,符合条件就会执行该函数。
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
很显然,这就是并发更新的逻辑,代码会最终调用 scheduleUpdateOnFiber,该方法是由 react-reconciler
提供,他后续会将任务带入到 scheduler
中调度。
// 与 enqueueConcurrentHookUpdateAndEagerlyBailout 方法逻辑
// 但会返回 root 节点
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
4
总结与思考
这就是 useState
的实现原理。其中包含了大量的逻辑操作,可能跟我们在使用时所想的那样有点不太一样。这里大量借助了闭包和链表结构来完成整个构想。
这个逻辑里面也会有大量的探讨存在于大厂面试的过程中。例如
一、为什么不能把 hook 的写法放到 if
判断中去;二、setState 的合并操作是如何做到的;三、hook 链表和 queue.pending 的环状链表都应该如何理解?四、setState 之后,为什么无法直接拿到最新值,彻底消化了之后这些问题都能很好的得到解答。
「React 知命境」 是一本从知识体系顶层出发,理论结合实践,通俗易懂,覆盖面广的精品小册,点击下方标签可阅读其他文章。欢迎关注我的公众号,我会持续更新。购买 React 哲学,或者赞赏本文 30 元,可进入 React 付费讨论群,学习氛围良好,学习进度加倍。赞赏之后也能看到 React 哲学的全部内容
原文始发于微信公众号(这波能反杀):烧脑预警,这波心智负担有点重,深度探讨 useState 的实现原理
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/240170.html