path:packages/react-reconciler/src/ReactFiberHooks.js
类型定义
Dispatcher 分发器
export type Dispatcher = { readContext( context: ReactContext , observedBits: void | number | boolean, ): T, useState (initialState: (() => S) | S): [S, Dispatch>], useReducer( reducer: (S, A) => S, initialArg: I, init?: (I) => S, ): [S, Dispatch ], useContext( context: ReactContext , observedBits: void | number | boolean, ): T, useRef (initialValue: T): { current: T}, useEffect( create: () => (() => void) | void, deps: Array | void | null, ): void, useLayoutEffect( create: () => (() => void) | void, deps: Array | void | null, ): void, useCallback (callback: T, deps: Array | void | null): T, useMemo (nextCreate: () => T, deps: Array | void | null): T, useImperativeHandle ( ref: { current: T | null} | ((inst: T | null) => mixed) | null | void, create: () => T, deps: Array | void | null, ): void, useDebugValue (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= { expirationTime: ExpirationTime, action: A, eagerReducer: ((S, A) => S) | null, eagerState: S | null, next: Update| null,};type UpdateQueue= { last: Update| 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| null, next: Effect,};export type FunctionComponentUpdateQueue = { lastEffect: Effect | null,};type BasicStateAction = (S => S) | S;type Dispatch = A => void;复制代码
Hook
export type Hook = { memoizedState: any, baseState: any, baseUpdate: Update| null, queue: UpdateQueue | null, next: Hook | null,};复制代码
renderWithHooks
在 一文中我已经看到,最重要的部分是 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( fiber: Fiber, queue: UpdateQueue, 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= { 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= { 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
不过错误调用信息的载体,它暴露同样的方法以能够在错误地调用的钩子时能正确回退。