「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」
今天继续学习 React 的源码,React 的仓库下有很多个模块,如 react、react-dom、react-reconciler、use-subscription等 package,这么多个模块到底从哪学起呢?
为什么想了解 react-reconciler
从目的上来讲,我想了解 React 的渲染机制、hook 是怎么设计的、修改 context 的值是怎么触发消费组件的更新的······今天我想学习一下 hook 的源码,联想到平时使用 hook 时都有这样的代码:
import { useContext, useEffect, useRef, useState } from 'react'
那么就先看一下 react 模块,果然在react源码仓库的packages/react/src/目录下找到了ReactHooks.js,但是令人失望的是,该文件的主要作用是暴露提供用户使用的 hook 方法,其中的函数实现基本都是调 dispatcher 对象的方法。通过查找函数声明和分析 import 语句,可以定位到 hook 的真正实现放在了 react-reconciler 模块的 src/ReactFiberHooks.new.js 文件夹下。
开始了解 ReactFiberHooks
这个文件有三千多行代码,我们先从 useState 开始研究。与之有关的有这几个函数:renderWithHooks、mountState、updateState、rerenderState。
renderWithHooks
先看一下 renderWithHooks 函数。这个函数会执行我们定义的函数式组件的渲染以及调度其中的 hook。
省略注释和非关键代码后如下所示:
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg);
if (didScheduleRenderPhaseUpdateDuringThisPass) {
let numberOfReRenders: number = 0;
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
if (numberOfReRenders >= RE_RENDER_LIMIT) {
throw new Error(
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
}
numberOfReRenders += 1;
currentHook = null;
workInProgressHook = null;
workInProgress.updateQueue = null;
ReactCurrentDispatcher.current = HooksDispatcherOnRerender;
children = Component(props, secondArg);
} while (didScheduleRenderPhaseUpdateDuringThisPass);
}
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
if (didRenderTooFewHooks) {
throw new Error(
'Rendered fewer hooks than expected. This may be caused by an accidental ' +
'early return statement.',
);
}
return children;
}
这个函数的逻辑是在执行 let children = Component(props, secondArg);前重置 workInProgress 的 updateQueue等状态,并根据 current 这个 Fiber 是否是第一次挂载选择不同的调度器(首次挂载用 HooksDispatcherOnMount,重新渲染用HooksDispatcherOnUpdate),这启示着我们下一步可以从二者之一如 HooksDispatcherOnMount 入手研究 hook。
显然,在执行 Component 函数的时候,可能会改变 didScheduleRenderPhaseUpdateDuringThisPass,这应该是用作渲染时更新触发重渲染的标识,当它为 true 时会再次执行 Component 函数,这可能形成一个无限循环,所以这里有一个最大重渲染次数的限制,一旦超过就报错。这种报错经常出现于“在 useEffect 里面执行 setState 操作,但是忘了给 useEffect 添加依赖项”的场景。
const MyComp = () => {
const [state, setState] = useState(null)
useEffect(() => {
setState(prev => prev + 1)
})
return <div>blabla</div>
}
还记得 react 不允许在 if 语句中使用 hook 的限制吗?一旦存在某个hook在第一次挂载被调用,在更新时没有被调用的情况,就会导致渲染后 currentHook.next !== null;,就会触发渲染 hook 太少的报错。
mountState
当我们跳转到 HooksDispatcherOnMount的定义时可以看到它给 useState字段分配的值是一个 mountState 函数。
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,
};
mountState 函数
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
interleaved: 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];
}
可以看到,我们平常使用 useState传的初始 state 被赋值给了 hook 的 memoizedState 字段,修改状态的函数 dispatch 的实现如下:
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),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
enqueueUpdate(fiber, queue, update, lane);
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
}
}
}
const eventTime = requestEventTime();
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (root !== null) {
entangleTransitionUpdate(root, queue, lane);
}
}
}
我们调用 setState 函数的入参就是这里的 action,它会作为 update对象的一部分传入enqueueRenderPhaseUpdate(queue, update);或enqueueUpdate(fiber, queue, update, lane);
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
) {
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
可以看到,我们在开头的 renderWithHooks 函数中看到的 didScheduleRenderPhaseUpdateDuringThisPass 在这里设成了 true,这也是 setState 能够触发重渲染的原理。
hook 原理
不仅是 mountState,所有在HooksDispatcherOnMount调度器中的 mount hook 的代码实现的第一行都是 const hook = mountWorkInProgressHook(); ,这是用于注册 hook,记录 memoizedState、queue等信息。与之对应的,HooksDispatcherOnUpdate调度器中的 update hook 也有个 updateWorkInProgressHook方法。
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 updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook;
if (currentHook === null) {
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) {
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;
}
这两个函数说明了一个组件上的所有 Hook 会组成一个单链表,挂载一个 hook 意味着创建一个链表节点,挂载在链表的尾部。在更新阶段第一次调用updateWorkInProgressHook拿到第一次挂载的hook,后面每次调用沿着链表不断取下一个 hook,如果到了尾部为空,则重新取链表头节点的 hook。为此框架才要求函数式组件中不能把hook的调用放在 if 语句中,这是为了每次调用 hook 函数能够以正确的顺序拿到hook 链表上的对应节点。
updateState
事实上,updateState 函数复用了与 useReducer 有关的updateReducer函数。
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
限于篇幅,这里不做展开,感兴趣的同学可以自行研究 react 的源码。