path:packages/react-reconciler/src/ReactFiberHooks.js
类型定义
Dispatcher 分发器
export type Dispatcher = {
readContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T,
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>],
useContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T,
useRef<T>(initialValue: T): {current: T},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T,
useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T,
useImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void,
useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void,
};
复制代码
Dispatcher
类型定义了 10 个 hook 方法的基本形式,并额外增加一个读取上下文的方法 readContext
。
Hooks 类型
export type HookType =
| 'useState'
| 'useReducer'
| 'useContext'
| 'useRef'
| 'useEffect'
| 'useLayoutEffect'
| 'useCallback'
| 'useMemo'
| 'useImperativeHandle'
| 'useDebugValue';
复制代码
Update、UpdateQueue、Effect
type Update<S, A> = {
expirationTime: ExpirationTime,
action: A,
eagerReducer: ((S, A) => S) | null,
eagerState: S | null,
next: Update<S, A> | null,
};
type UpdateQueue<S, A> = {
last: Update<S, A> | null,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};
type Effect = {
tag: HookEffectTag,
create: () => (() => void) | void,
destroy: (() => void) | void,
deps: Array<mixed> | null,
next: Effect,
};
export type FunctionComponentUpdateQueue = {
lastEffect: Effect | null,
};
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;
复制代码
Hook
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,
};
复制代码
renderWithHooks
在 React 深度学习:ReactHooks 一文中我已经看到,最重要的部分是 ReactCurrentDispatcher
对象,它有一个 current
属性,这个属性是 Dispatcher
类型的对象。但到目前我们还没有见到这个对象的初始化,或者叫对象的具体实现。
通读 ReactFiberHooks.js
的代码,发现有几个方法中有对 ReactCurrentDispatcher.current
赋值的代码:
renderWithHooks
resetHooks
dispatchAction
当然,为了严谨,我用 IDEA 查找了所有对 ReactCurrentDispatcher.current
的引用,未发现其他有使用到的地方。
renderWithHooks
export function renderWithHooks(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
props: any,
refOrContext: any,
nextRenderExpirationTime: ExpirationTime,
): any {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
nextCurrentHook = current !== null ? current.memoizedState : null;
if (__DEV__) {
// do something
}
// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;
// remainingExpirationTime = NoWork;
// componentUpdateQueue = null;
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;
// sideEffectTag = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because nextCurrentHook === null.
// This is tricky because it's valid for certain types of components (e.g. React.lazy)
// Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.
// Non-stateful hooks (e.g. context) don't get added to memoizedState,
// so nextCurrentHook would be null during updates and mounts.
if (__DEV__) {
// do something
} else {
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
let children = Component(props, refOrContext);
if (didScheduleRenderPhaseUpdate) {
do {
didScheduleRenderPhaseUpdate = false;
numberOfReRenders += 1;
// 从列表的开头重新开始
nextCurrentHook = current !== null ? current.memoizedState : null;
nextWorkInProgressHook = firstWorkInProgressHook;
currentHook = null;
workInProgressHook = null;
componentUpdateQueue = null;
if (__DEV__) {
// do something
}
ReactCurrentDispatcher.current = __DEV__
? HooksDispatcherOnUpdateInDEV
: HooksDispatcherOnUpdate;
children = Component(props, refOrContext);
} while (didScheduleRenderPhaseUpdate);
renderPhaseUpdates = null;
numberOfReRenders = 0;
}
// We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there's no re-entrancy.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const renderedWork: Fiber = (currentlyRenderingFiber: any);
renderedWork.memoizedState = firstWorkInProgressHook;
renderedWork.expirationTime = remainingExpirationTime;
renderedWork.updateQueue = (componentUpdateQueue: any);
renderedWork.effectTag |= sideEffectTag;
if (__DEV__) {
// do something
}
// This check uses currentHook so that it works the same in DEV and prod bundles.
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
currentHook = null;
nextCurrentHook = null;
firstWorkInProgressHook = null;
workInProgressHook = null;
nextWorkInProgressHook = null;
if (__DEV__) {
// do something
}
remainingExpirationTime = NoWork;
componentUpdateQueue = null;
sideEffectTag = 0;
// These were reset above
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;
invariant(
!didRenderTooFewHooks,
'Rendered fewer hooks than expected. This may be caused by an accidental ' +
'early return statement.',
);
return children;
}
复制代码
上面的代码中有有两处赋值:
ReactCurrentDispatcher.current = __DEV__
? HooksDispatcherOnUpdateInDEV
: HooksDispatcherOnUpdate;
复制代码
和
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
复制代码
resetHooks
export function resetHooks(): void {
// We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there's no re-entrancy.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
// This is used to reset the state of this module when a component throws.
// It's also called inside mountIndeterminateComponent if we determine the
// component is a module-style component.
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
currentHook = null;
nextCurrentHook = null;
firstWorkInProgressHook = null;
workInProgressHook = null;
nextWorkInProgressHook = null;
if (__DEV__) {
// do something
}
remainingExpirationTime = NoWork;
componentUpdateQueue = null;
sideEffectTag = 0;
didScheduleRenderPhaseUpdate = false;
renderPhaseUpdates = null;
numberOfReRenders = 0;
}
复制代码
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
复制代码
dispatchAction
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
if (__DEV__) {
// do something
}
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdate = true;
const update: Update<S, A> = {
expirationTime: renderExpirationTime,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
const 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;
}
} else {
flushPassiveEffects();
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update: Update<S, A> = {
expirationTime,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// Append the update to the end of the list.
const last = queue.last;
if (last === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
const first = last.next;
if (first !== null) {
// Still circular.
update.next = first;
}
last.next = update;
}
queue.last = update;
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
if (__DEV__) {
prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
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.
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
if (__DEV__) {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
}
}
if (__DEV__) {
// do something
}
scheduleWork(fiber, expirationTime);
}
}
复制代码
if (__DEV__) {
prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
复制代码
和
if (__DEV__) {
ReactCurrentDispatcher.current = prevDispatcher;
}
复制代码
但这都是开发环境的逻辑,不作过去阐述。
在上面三个方法中,我们看到 ReactCurrentDispatcher.current
分被赋予了如下值:
-
HooksDispatcherOnUpdate
-
ContextOnlyDispatcher
-
InvalidNestedHooksDispatcherOnUpdateInDEV
HooksDispatcherOnUpdate
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
};
复制代码
ContextOnlyDispatcher
export const ContextOnlyDispatcher: Dispatcher = {
readContext,
useCallback: throwInvalidHookError,
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
useRef: throwInvalidHookError,
useState: throwInvalidHookError,
useDebugValue: throwInvalidHookError,
};
复制代码
-
throwInvalidHookError 方法:
function throwInvalidHookError() { invariant( false, 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + ' one of the following reasons:\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + '2. You might be breaking the Rules of Hooks\n' + '3. You might have more than one copy of React in the same app\n' + 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.', ); } 复制代码
该方法直接抛出了一个错误,错误信息说明这是一个无效的 hook 调用,原因:
- hook 只能在函数组件的主体内部调用
- 有 React 版本和呈现器(如React DOM) 不匹配
- 可能违反了 hook 的规则
- 可能在同一个应用程序中有多个 React 副本
通过以上信息我们不难看出,ContextOnlyDispatcher
不过错误调用信息的载体,它暴露同样的方法以能够在错误地调用的钩子时能正确回退。