跳到主要内容

认识

2023年06月10日
柏拉文
越努力,越幸运

处理策略


hooks 对象本质上是主要以三种处理策略存在 React 中:

ContextOnlyDispatcher

ContextOnlyDispatcher 第一种形态是防止开发者在函数组件外部调用hooks,所以第一种就是报错形态,只要开发者调用了这个形态下的 hooks ,就会抛出异常。

/react/packages/react-reconciler/src/ReactFiberHooks.new.js
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,
useDeferredValue: throwInvalidHookError,
useTransition: throwInvalidHookError,
useMutableSource: throwInvalidHookError,
useOpaqueIdentifier: throwInvalidHookError,

unstable_isNewReconciler: enableNewReconciler,
};

HooksDispatcherOnMount

HooksDispatcherOnMount 第二种形态是函数组件初始化 mount ,因为之前讲过 hooks 是函数组件和对应 fiber 桥梁,这个时候的 hooks 作用就是建立这个桥梁,初次建立其 hooksfiber 之间的关系。

/react/packages/react-reconciler/src/ReactFiberHooks.new.js
const HooksDispatcherOnMount: Dispatcher = {
readContext,

useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useOpaqueIdentifier: mountOpaqueIdentifier,

unstable_isNewReconciler: enableNewReconciler,
};

HooksDispatcherOnUpdate

HooksDispatcherOnUpdate: 第三种形态是函数组件的更新,既然与 fiber 之间的桥已经建好了,那么组件再更新,就需要 hooks 去获取或者更新维护状态。

/react/packages/react-reconciler/src/ReactFiberHooks.new.js
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,

useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useOpaqueIdentifier: updateOpaqueIdentifier,

unstable_isNewReconciler: enableNewReconciler,
};

数据结构


function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,

baseState: null,
baseQueue: null,
queue: {
pending: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;
}

字段含义

  • memoizedState: 不同类型hookmemoizedState保存不同类型数据,具体如下:

    • useState: 对于const [state, updateState] = useState(initialState), memoizedState保存state的值

    • useReducer: 对于const [state, dispatch] = useReducer(reducer, {}), memoizedState保存state的值

    • useEffect: memoizedState保存包含useEffect回调函数、依赖项等的链表数据结构effecteffect 链表同时会保存在fiber.updateQueue

    • useRef: 对于useRef(1)memoizedState保存{current: 1}

    • useMemo: 对于useMemo(callback, [depA])memoizedState保存[callback(), depA]

    • useCallback: 对于useCallback(callback, [depA])memoizedState保存[callback, depA]。与useMemo的区别是,useCallback保存的是callback函数本身,而useMemo保存的是callback函数的执行结果

    • useContext: 没有memoizedState

    注意

    一、hookFunctionComponent Fiber 都存在 memoizedState 属性, 不要混肴它们的概念

    1. fiber.memoizedState: FunctionComponent对应fiber保存的Hooks链表。在 FunctionComponent 中, 多个 hook 会形成 hook 链表,保存在 FibermemoizedState 上。

    2. hook.memoizedState: Hooks链表中保存的单一hook对应的数据。

  • baseState: 初始state

  • baseQueue: 初始queue队列

  • queue: 将需要更新的update保存在hook.queue.pending

  • next: 下一个hook

Hooks 类型


类型 Flags

/react/packages/react-reconciler/src/ReactHookEffectTags.js
export const NoFlags = /*  */ 0b000;    // 0  无副作用
export const HasEffect = /* */ 0b001; // 1 有副作用
export const Layout = /* */ 0b010; // 2 useLayoutEffect
export const Passive = /* */ 0b100; // 4 useEffect

Flags 位运算

useLayoutEffect

const layoutTag = HasEffect|Layout;
if(layoutTag&Layout !== NoFlags){
console.log("useLayoutEffect");
}

useEffect

const effectTag = HasEffect|Passive;
if(effectTag&Passive !== NoFlags){
console.log("useEffect");
}

运行流程-声明阶段


FunctionComponent 组件进入 render阶段对应的beginWork时,会调用 renderWithHooks , 该方法内部会执行 FunctionComponent 对应函数(即 fiber.type)。所有函数组件的触发是在 renderWithHooks 方法中

beginWork 源码如下:

/react/packages/react-reconciler/src/ReactFiberBeginWork.new.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const updateLanes = workInProgress.lanes;

if (__DEV__) {
if (workInProgress._debugNeedsRemount && current !== null) {
// This will restart the begin phase with a new fiber.
return remountFiber(
current,
workInProgress,
createFiberFromTypeAndProps(
workInProgress.type,
workInProgress.key,
workInProgress.pendingProps,
workInProgress._debugOwner || null,
workInProgress.mode,
workInProgress.lanes,
),
);
}
}

if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;

if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
// This fiber does not have any pending work. Bailout without entering
// the begin phase. There's still some bookkeeping we that needs to be done
// in this optimized path, mostly pushing stuff onto the stack.
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
resetHydrationState();
break;
case HostComponent:
pushHostContext(workInProgress);
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
break;
}
case HostPortal:
pushHostContainer(
workInProgress,
workInProgress.stateNode.containerInfo,
);
break;
case ContextProvider: {
const newValue = workInProgress.memoizedProps.value;
pushProvider(workInProgress, newValue);
break;
}
case Profiler:
if (enableProfilerTimer) {
// Reset effect durations for the next eventual effect phase.
// These are reset during render to allow the DevTools commit hook a chance to read them,
const stateNode = workInProgress.stateNode;
stateNode.effectDuration = 0;
stateNode.passiveEffectDuration = 0;
}
break;
case SuspenseComponent: {
const state: SuspenseState | null = workInProgress.memoizedState;
if (state !== null) {
if (enableSuspenseServerRenderer) {
if (state.dehydrated !== null) {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
// We know that this component will suspend again because if it has
// been unsuspended it has committed as a resolved Suspense component.
// If it needs to be retried, it should have work scheduled on it.
workInProgress.flags |= DidCapture;
// We should never render the children of a dehydrated boundary until we
// upgrade it. We return null instead of bailoutOnAlreadyFinishedWork.
return null;
}
}

// If this boundary is currently timed out, we need to decide
// whether to retry the primary children, or to skip over it and
// go straight to the fallback. Check the priority of the primary
// child fragment.
const primaryChildFragment: Fiber = (workInProgress.child: any);
const primaryChildLanes = primaryChildFragment.childLanes;
if (includesSomeLane(renderLanes, primaryChildLanes)) {
// The primary children have pending work. Use the normal path
// to attempt to render the primary children again.
return updateSuspenseComponent(
current,
workInProgress,
renderLanes,
);
} else {
// The primary child fragment does not have pending work marked
// on it
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
// The primary children do not have pending work with sufficient
// priority. Bailout.
const child = bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
if (child !== null) {
// The fallback children have pending work. Skip over the
// primary children and work on the fallback.
return child.sibling;
} else {
return null;
}
}
} else {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
}
break;
}
case SuspenseListComponent: {
const didSuspendBefore = (current.flags & DidCapture) !== NoFlags;

const hasChildWork = includesSomeLane(
renderLanes,
workInProgress.childLanes,
);

if (didSuspendBefore) {
if (hasChildWork) {
// If something was in fallback state last time, and we have all the
// same children then we're still in progressive loading state.
// Something might get unblocked by state updates or retries in the
// tree which will affect the tail. So we need to use the normal
// path to compute the correct tail.
return updateSuspenseListComponent(
current,
workInProgress,
renderLanes,
);
}
// If none of the children had any work, that means that none of
// them got retried so they'll still be blocked in the same way
// as before. We can fast bail out.
workInProgress.flags |= DidCapture;
}

// If nothing suspended before and we're rendering the same children,
// then the tail doesn't matter. Anything new that suspends will work
// in the "together" mode, so we can continue from the state we had.
const renderState = workInProgress.memoizedState;
if (renderState !== null) {
// Reset to the "together" mode in case we've started a different
// update in the past but didn't complete it.
renderState.rendering = null;
renderState.tail = null;
}
pushSuspenseContext(workInProgress, suspenseStackCursor.current);

if (hasChildWork) {
break;
} else {
// If none of the children had any work, that means that none of
// them got retried so they'll still be blocked in the same way
// as before. We can fast bail out.
return null;
}
}
case OffscreenComponent:
case LegacyHiddenComponent: {
// Need to check if the tree still needs to be deferred. This is
// almost identical to the logic used in the normal update path,
// so we'll just enter that. The only difference is we'll bail out
// at the next level instead of this one, because the child props
// have not changed. Which is fine.
// TODO: Probably should refactor `beginWork` to split the bailout
// path from the normal path. I'm tempted to do a labeled break here
// but I won't :)
workInProgress.lanes = NoLanes;
return updateOffscreenComponent(current, workInProgress, renderLanes);
}
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
// See https://github.com/facebook/react/pull/19216.
didReceiveUpdate = true;
} else {
// An update was scheduled on this fiber, but there are no new props
// nor legacy context. Set this to false. If an update queue or context
// consumer produces a changed value, it will set this to true. Otherwise,
// the component will assume the children have not changed and bail out.
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
}

// Before entering the begin phase, clear pending update priority.
// TODO: This assumes that we're about to evaluate the component and process
// the update queue. However, there's an exception: SimpleMemoComponent
// sometimes bails out later in the begin phase. This indicates that we should
// move this assignment out of the common path and into each branch.
workInProgress.lanes = NoLanes;

switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateLanes,
renderLanes,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
case ForwardRef: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === type
? unresolvedProps
: resolveDefaultProps(type, unresolvedProps);
return updateForwardRef(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case Mode:
return updateMode(current, workInProgress, renderLanes);
case Profiler:
return updateProfiler(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
const outerPropTypes = type.propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
resolvedProps, // Resolved for outer only
'prop',
getComponentName(type),
);
}
}
}
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
updateLanes,
renderLanes,
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
updateLanes,
renderLanes,
);
}
case IncompleteClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case SuspenseListComponent: {
return updateSuspenseListComponent(current, workInProgress, renderLanes);
}
case FundamentalComponent: {
if (enableFundamentalAPI) {
return updateFundamentalComponent(current, workInProgress, renderLanes);
}
break;
}
case ScopeComponent: {
if (enableScopeAPI) {
return updateScopeComponent(current, workInProgress, renderLanes);
}
break;
}
case Block: {
if (enableBlocksAPI) {
const block = workInProgress.type;
const props = workInProgress.pendingProps;
return updateBlock(current, workInProgress, block, props, renderLanes);
}
break;
}
case OffscreenComponent: {
return updateOffscreenComponent(current, workInProgress, renderLanes);
}
case LegacyHiddenComponent: {
return updateLegacyHiddenComponent(current, workInProgress, renderLanes);
}
}
invariant(
false,
'Unknown unit of work tag (%s). This error is likely caused by a bug in ' +
'React. Please file an issue.',
workInProgress.tag,
);
}

updateFunctionComponent 源码如下:

/react/packages/react-reconciler/src/ReactFiberBeginWork.new.js
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes,
) {
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
// Lazy component props can't be validated in createElement
// because they're only guaranteed to be resolved here.
const innerPropTypes = Component.propTypes;
if (innerPropTypes) {
checkPropTypes(
innerPropTypes,
nextProps, // Resolved props
'prop',
getComponentName(Component),
);
}
}
}

let context;
if (!disableLegacyContext) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
context = getMaskedContext(workInProgress, unmaskedContext);
}

let nextChildren;
prepareToReadContext(workInProgress, renderLanes);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
setIsRendering(true);
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode
) {
disableLogs();
try {
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
} finally {
reenableLogs();
}
}
setIsRendering(false);
} else {
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
}

if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}

renderWithHooks 源码如下:

/react/packages/react-reconciler/src/ReactFiberHooks.new.js
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;

if (__DEV__) {
hookTypesDev =
current !== null
? ((current._debugHookTypes: any): Array<HookType>)
: null;
hookTypesUpdateIndexDev = -1;
// Used for hot reloading:
ignorePreviousDependencies =
current !== null && current.type !== workInProgress.type;
}

workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;

// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;

// didScheduleRenderPhaseUpdate = false;

// 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 memoizedState === null.
// This is tricky because it's valid for certain types of components (e.g. React.lazy)

// Using memoizedState 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 memoizedState would be null during updates and mounts.
if (__DEV__) {
if (current !== null && current.memoizedState !== null) {
ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
} else if (hookTypesDev !== null) {
// This dispatcher handles an edge case where a component is updating,
// but no stateful hooks have been used.
// We want to match the production code behavior (which will use HooksDispatcherOnMount),
// but with the extra DEV validation to ensure hooks ordering hasn't changed.
// This dispatcher does that.
ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
}
} else {
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}

let children = Component(props, secondArg);

// Check if there was a render phase update
if (didScheduleRenderPhaseUpdateDuringThisPass) {
// Keep rendering in a loop for as long as render phase updates continue to
// be scheduled. Use a counter to prevent infinite loops.
let numberOfReRenders: number = 0;
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);

numberOfReRenders += 1;
if (__DEV__) {
// Even when hot reloading, allow dependencies to stabilize
// after first render to prevent infinite render phase updates.
ignorePreviousDependencies = false;
}

// Start over from the beginning of the list
currentHook = null;
workInProgressHook = null;

workInProgress.updateQueue = null;

if (__DEV__) {
// Also validate hook order for cascading updates.
hookTypesUpdateIndexDev = -1;
}

ReactCurrentDispatcher.current = __DEV__
? HooksDispatcherOnRerenderInDEV
: HooksDispatcherOnRerender;

children = Component(props, secondArg);
} while (didScheduleRenderPhaseUpdateDuringThisPass);
}

// 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;

if (__DEV__) {
workInProgress._debugHookTypes = hookTypesDev;
}

// 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;

renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);

currentHook = null;
workInProgressHook = null;

if (__DEV__) {
currentHookNameInDev = null;
hookTypesDev = null;
hookTypesUpdateIndexDev = -1;
}

didScheduleRenderPhaseUpdate = false;

invariant(
!didRenderTooFewHooks,
'Rendered fewer hooks than expected. This may be caused by an accidental ' +
'early return statement.',
);

return children;
}

HooksDispatcherOnMountHooksDispatcherOnUpdate 源码如下:

/react/packages/react-reconciler/src/ReactFiberHooks.new.js
const HooksDispatcherOnMount: Dispatcher = {
readContext,

useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useOpaqueIdentifier: mountOpaqueIdentifier,

unstable_isNewReconciler: enableNewReconciler,
};

const HooksDispatcherOnUpdate: Dispatcher = {
readContext,

useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useOpaqueIdentifier: updateOpaqueIdentifier,

unstable_isNewReconciler: enableNewReconciler,
};

解读: Hooks中,组件mount时的hookupdate时的hook来源于不同的对象,这类对象在源码中被称为dispatcher。在FunctionComponent render前,会根据FunctionComponent对应fiber的条件区分mountupdate,并将不同情况对应的dispatcher赋值给全局变量ReactCurrentDispatchercurrent属性。

useHooks 源码如下:

  • resolveDispatcher 源码:

    /react/packages/react/src/ReactHooks.js
    function resolveDispatcher() {
    const dispatcher = ReactCurrentDispatcher.current;
    invariant(
    dispatcher !== null,
    '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://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
    );
    return dispatcher;
    }
  • useContext 源码:

    /react/packages/react/src/ReactHooks.js
    export function useContext<T>(
    Context: ReactContext<T>,
    unstable_observedBits: number | boolean | void,
    ): T {
    const dispatcher = resolveDispatcher();
    if (__DEV__) {
    if (unstable_observedBits !== undefined) {
    console.error(
    'useContext() second argument is reserved for future ' +
    'use in React. Passing it is not supported. ' +
    'You passed: %s.%s',
    unstable_observedBits,
    typeof unstable_observedBits === 'number' && Array.isArray(arguments[2])
    ? '\n\nDid you call array.map(useContext)? ' +
    'Calling Hooks inside a loop is not supported. ' +
    'Learn more at https://reactjs.org/link/rules-of-hooks'
    : '',
    );
    }

    // TODO: add a more generic warning for invalid values.
    if ((Context: any)._context !== undefined) {
    const realContext = (Context: any)._context;
    // Don't deduplicate because this legitimately causes bugs
    // and nobody should be using this in existing code.
    if (realContext.Consumer === Context) {
    console.error(
    'Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be ' +
    'removed in a future major release. Did you mean to call useContext(Context) instead?',
    );
    } else if (realContext.Provider === Context) {
    console.error(
    'Calling useContext(Context.Provider) is not supported. ' +
    'Did you mean to call useContext(Context) instead?',
    );
    }
    }
    }
    return dispatcher.useContext(Context, unstable_observedBits);
    }
  • useState 源码

    /react/packages/react/src/ReactHooks.js
    export function useState<S>(
    initialState: (() => S) | S,
    ): [S, Dispatch<BasicStateAction<S>>] {
    const dispatcher = resolveDispatcher();
    return dispatcher.useState(initialState);
    }
  • useReducer 源码

    /react/packages/react/src/ReactHooks.js
    export function useReducer<S, I, A>(
    reducer: (S, A) => S,
    initialArg: I,
    init?: I => S,
    ): [S, Dispatch<A>] {
    const dispatcher = resolveDispatcher();
    return dispatcher.useReducer(reducer, initialArg, init);
    }
  • useRef 源码

    export function useRef<T>(initialValue: T): {|current: T|} {
    const dispatcher = resolveDispatcher();
    return dispatcher.useRef(initialValue);
    }
  • useEffect 源码

    export function useEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
    ): void {
    const dispatcher = resolveDispatcher();
    return dispatcher.useEffect(create, deps);
    }
  • useLayoutEffect 源码

    export function useLayoutEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
    ): void {
    const dispatcher = resolveDispatcher();
    return dispatcher.useLayoutEffect(create, deps);
    }
  • useCallback 源码

    export function useCallback<T>(
    callback: T,
    deps: Array<mixed> | void | null,
    ): T {
    const dispatcher = resolveDispatcher();
    return dispatcher.useCallback(callback, deps);
    }
  • useMemo 源码

    export function useMemo<T>(
    create: () => T,
    deps: Array<mixed> | void | null,
    ): T {
    const dispatcher = resolveDispatcher();
    return dispatcher.useMemo(create, deps);
    }

解读: 在FunctionComponent render时,会从ReactCurrentDispatcher.current(即当前dispatcher)中寻找需要的hook。换言之,不同的调用栈上下文为ReactCurrentDispatcher.current赋值不同的dispatcher,则FunctionComponent render时调用的hook也是不同的函数。下面分别从mount时和update时分别讲解

mount 时

mount时,ReactCurrentDispatcher.current = HooksDispatcherOnMount , HooksDispatcherOnMount 源码如下:

/react/packages/react-reconciler/src/ReactFiberHooks.new.js
const HooksDispatcherOnMount: Dispatcher = {
readContext,

useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useOpaqueIdentifier: mountOpaqueIdentifier,

unstable_isNewReconciler: enableNewReconciler,
};

假设函数中遇到useReducer,那么useReducer会调用mountReducer;遇到useState,那么useState会调用mountState, 源码如下:

  • mountReducer

    /react/packages/react-reconciler/src/ReactFiberHooks.new.js
    function mountReducer<S, I, A>(
    reducer: (S, A) => S,
    initialArg: I,
    init?: I => S,
    ): [S, Dispatch<A>] {
    const hook = mountWorkInProgressHook();
    let initialState;
    if (init !== undefined) {
    initialState = init(initialArg);
    } else {
    initialState = ((initialArg: any): S);
    }
    hook.memoizedState = hook.baseState = initialState;
    const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
    });
    const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
    ): any));
    return [hook.memoizedState, dispatch];
    }
  • 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 = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
    });
    const dispatch: Dispatch<
    BasicStateAction<S>,
    > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
    ): any));
    return [hook.memoizedState, dispatch];
    }

update 时

update时,ReactCurrentDispatcher.current = HooksDispatcherOnUpdate , HooksDispatcherOnUpdate 源码如下:

/react/packages/react-reconciler/src/ReactFiberHooks.new.js
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,

useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useOpaqueIdentifier: updateOpaqueIdentifier,

unstable_isNewReconciler: enableNewReconciler,
};

假设函数中遇到useReducer,那么useReducer会调用updateReducer;遇到useState,那么useState会调用updateState, 源码如下:

  • updateReducer

    function updateReducer<S, I, A>(
    reducer: (S, A) => S,
    initialArg: I,
    init?: I => S,
    ): [S, Dispatch<A>] {
    const hook = updateWorkInProgressHook();
    const queue = hook.queue;
    invariant(
    queue !== null,
    'Should have a queue. This is likely a bug in React. Please file an issue.',
    );

    queue.lastRenderedReducer = reducer;

    const current: Hook = (currentHook: any);

    // The last rebase update that is NOT part of the base state.
    let baseQueue = current.baseQueue;

    // The last pending update that hasn't been processed yet.
    const pendingQueue = queue.pending;
    if (pendingQueue !== null) {
    // We have new updates that haven't been processed yet.
    // We'll add them to the base queue.
    if (baseQueue !== null) {
    // Merge the pending queue and the base queue.
    const baseFirst = baseQueue.next;
    const pendingFirst = pendingQueue.next;
    baseQueue.next = pendingFirst;
    pendingQueue.next = baseFirst;
    }
    if (__DEV__) {
    if (current.baseQueue !== baseQueue) {
    // Internal invariant that should never happen, but feasibly could in
    // the future if we implement resuming, or some form of that.
    console.error(
    'Internal error: Expected work-in-progress queue to be a clone. ' +
    'This is a bug in React.',
    );
    }
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
    }

    if (baseQueue !== null) {
    // We have a queue to process.
    const first = baseQueue.next;
    let newState = current.baseState;

    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
    const updateLane = update.lane;
    if (!isSubsetOfLanes(renderLanes, updateLane)) {
    // Priority is insufficient. Skip this update. If this is the first
    // skipped update, the previous update/state is the new base
    // update/state.
    const clone: Update<S, A> = {
    lane: updateLane,
    action: update.action,
    eagerReducer: update.eagerReducer,
    eagerState: update.eagerState,
    next: (null: any),
    };
    if (newBaseQueueLast === null) {
    newBaseQueueFirst = newBaseQueueLast = clone;
    newBaseState = newState;
    } else {
    newBaseQueueLast = newBaseQueueLast.next = clone;
    }
    // Update the remaining priority in the queue.
    // TODO: Don't need to accumulate this. Instead, we can remove
    // renderLanes from the original lanes.
    currentlyRenderingFiber.lanes = mergeLanes(
    currentlyRenderingFiber.lanes,
    updateLane,
    );
    markSkippedUpdateLanes(updateLane);
    } else {
    // This update does have sufficient priority.

    if (newBaseQueueLast !== null) {
    const clone: Update<S, A> = {
    // This update is going to be committed so we never want uncommit
    // it. Using NoLane works because 0 is a subset of all bitmasks, so
    // this will never be skipped by the check above.
    lane: NoLane,
    action: update.action,
    eagerReducer: update.eagerReducer,
    eagerState: update.eagerState,
    next: (null: any),
    };
    newBaseQueueLast = newBaseQueueLast.next = clone;
    }

    // Process this update.
    if (update.eagerReducer === reducer) {
    // If this update was processed eagerly, and its reducer matches the
    // current reducer, we can use the eagerly computed state.
    newState = ((update.eagerState: any): S);
    } else {
    const action = update.action;
    newState = reducer(newState, action);
    }
    }
    update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
    newBaseState = newState;
    } else {
    newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    // Mark that the fiber performed work, but only if the new state is
    // different from the current state.
    if (!is(newState, hook.memoizedState)) {
    markWorkInProgressReceivedUpdate();
    }

    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
    }

    const dispatch: Dispatch<A> = (queue.dispatch: any);
    return [hook.memoizedState, dispatch];
    }

    工作: 找到对应的hook,根据update计算该hook的新state并返回

  • updateState

    function updateState<S>(
    initialState: (() => S) | S,
    ): [S, Dispatch<BasicStateAction<S>>] {
    return updateReducer(basicStateReducer, (initialState: any));
    }

问题

  • mount时获取当前hook使用的是mountWorkInProgressHook,而update时使用的是updateWorkInProgressHook,这里的原因是:

    • mount时可以确定是调用ReactDOM.render或相关初始化API产生的更新,只会执行一次。

    • update可能是在事件回调或副作用中触发的更新或者是render阶段触发的更新,为了避免组件无限循环更新,后者需要区别对待。

运行流程-调用阶段


调用阶段会执行dispatchAction,此时该FunctionComponent对应的Fiber以及hook.queue已经通过调用bind方法预先作为参数传入,dispatchAction 源码如下:

function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
if (__DEV__) {
if (typeof arguments[3] === 'function') {
console.error(
"State updates from the useState() and useReducer() Hooks don't support the " +
'second callback argument. To execute a side effect after ' +
'rendering, declare it in the component body with useEffect().',
);
}
}

const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber);

const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};

// Append the update to the end of the list.
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;

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.
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
} else {
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// 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__) {
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if (typeof jest !== 'undefined') {
warnIfNotScopedWithMatchingAct(fiber);
warnIfNotCurrentlyActingUpdatesInDev(fiber);
}
}
scheduleUpdateOnFiber(fiber, lane, eventTime);
}

if (__DEV__) {
if (enableDebugTracing) {
if (fiber.mode & DebugTracingMode) {
const name = getComponentName(fiber.type) || 'Unknown';
logStateUpdateScheduled(name, lane, action);
}
}
}

if (enableSchedulingProfiler) {
markStateUpdateScheduled(fiber, lane);
}
}

工作: 创建update,将update加入queue.pending中,并开启调度。

详解:

  • currentlyRenderingFiberworkInProgress,workInProgress存在代表当前处于render阶段。触发更新时通过bind预先保存的fiberworkInProgress全等,代表本次更新发生于FunctionComponent对应fiberrender阶段。所以这是一个render阶段触发的更新,需要标记变量didScheduleRenderPhaseUpdate,后续单独处理。

    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.
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  • fiber.lanes保存fiber上存在的update的优先级。fiber.lanes === NoLanes意味着fiber上不存在update。我们已经知道,通过update计算state发生在声明阶段,这是因为该hook上可能存在多个不同优先级的update,最终state的值由多个update共同决定。但是当fiber上不存在update,则调用阶段创建的update为该hook上第一个update,在声明阶段计算state时也只依赖于该update,完全不需要进入声明阶段再计算state。这样做的好处是:如果计算出的state与该hook之前保存的state一致,那么完全不需要开启一次调度。即使计算出的state与该hook之前保存的state不一致,在声明阶段也可以直接使用调用阶段已经计算出的state

    if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes))

经典问题


一、为什么不能在条件和循环里使用Hooks?

答: Hook 对象结构为:

function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: {
pending: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;
}

Hook 对象的 memoizedState 属性就是用来存储组件上一次更新后的 statenext 指向下一个 hook 对象。在组件更新的过程中,如果 hooks 函数执行的顺序是不变的,就可以根据这个链表拿到当前 hooks 对应的 Hook 对象。因此要确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useStateuseEffect 调用之间保持 hook 状态的正确。

二、为什么不能在函数组件外部使用Hooks?