react source code analysis 13.hooks source code
Video Explanation (efficient learning): Enter learning
Previous articles:
1. Introduction and interview questions
3.react source code architecture
4. Source directory structure and debugging
6.legacy and concurrent mode entry functions
20. Summary & answers to interview questions in Chapter 1
hook call entry
In the hook source code, hook exists in Dispatcher, which is an object. The functions called by different hooks are different. The global variable ReactCurrentDispatcher.current will be assigned to HooksDispatcherOnMount or HooksDispatcherOnUpdate according to mount or update
ReactCurrentDispatcher.current = current === null || current.memoizedState === null//mount or update ? HooksDispatcherOnMount : HooksDispatcherOnUpdate;
const HooksDispatcherOnMount: Dispatcher = {//mount time useCallback: mountCallback, useContext: readContext, useEffect: mountEffect, useImperativeHandle: mountImperativeHandle, useLayoutEffect: mountLayoutEffect, useMemo: mountMemo, useReducer: mountReducer, useRef: mountRef, useState: mountState, //... }; const HooksDispatcherOnUpdate: Dispatcher = {//update useCallback: updateCallback, useContext: readContext, useEffect: updateEffect, useImperativeHandle: updateImperativeHandle, useLayoutEffect: updateLayoutEffect, useMemo: updateMemo, useReducer: updateReducer, useRef: updateRef, useState: updateState, //... };
hook data structure
In the FunctionComponent, multiple hooks will form a hook linked list, which is saved in the memoizedState of Fiber, and the Update to be updated is saved in hook.queue.pending
const hook: Hook = { memoizedState: null,//There are different values for different hook s baseState: null,//Initial state baseQueue: null,//Initial queue queue: null,//Update to update next: null,//Next hook };
Let's look at the values corresponding to memoizedState
- useState: for example, const [state, updateState] = useState(initialState), memoizedState equals the value of state
- useReducer: for example, const [state, dispatch] = useReducer(reducer, {});, memoizedState is equal to the value of state
- useEffect: during mountEffect, pushEffect will be called to create the effect linked list. memoizedState is equal to the effect linked list. The effect linked list will also be attached to fiber.updateQueue. The first parameter callback and the second parameter dependency array of useEffect exist on each effect. For example, useEffect(callback, [dep]), and the effect is {create:callback, dep:dep,...}
- useRef: for example, useRef(0), memoizedState is equal to {current: 0}
- useMemo: for example, useMemo(callback, [dep]), memoizedState equals [callback(), dep]
- useCallback: for example, useCallback(callback, [dep]), memoizedState equals [callback, dep]. useCallback saves the callback function, and useMemo saves the execution result of the callback
useState&useReducer
The reason why useState and useReducer are put together is that in the source code, useState is a useReducer with default reducer parameters.
-
Usestate & usereducer declaration
The resolveDispatcher function gets the current Dispatcher
function useState(initialState) { var dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); } function useReducer(reducer, initialArg, init) { var dispatcher = resolveDispatcher(); return dispatcher.useReducer(reducer, initialArg, init); }
-
mount phase
In the mount stage, useState calls mountState and useReducer calls mountReducer. The only difference is that the lastRenderedReducer in the queue they create is different. Mount has an initial value of basicStateReducer. Therefore, useState is a useReducer with default reducer parameters.
function mountState<S>(// initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { const hook = mountWorkInProgressHook();//Create current hook if (typeof initialState === 'function') { initialState = initialState(); } hook.memoizedState = hook.baseState = initialState;//hook.memoizedState assignment const queue = (hook.queue = {//Assign hook.queue pending: null, dispatch: null, lastRenderedReducer: basicStateReducer,//Difference between and mountReducer lastRenderedState: (initialState: any), }); const dispatch: Dispatch<//Create dispatch function BasicStateAction<S>, > = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, queue, ): any)); return [hook.memoizedState, dispatch];//Return memoizedState and dispatch } function mountReducer<S, I, A>( reducer: (S, A) => S, initialArg: I, init?: I => S, ): [S, Dispatch<A>] { const hook = mountWorkInProgressHook();//Create current hook let initialState; if (init !== undefined) { initialState = init(initialArg); } else { initialState = ((initialArg: any): S); } hook.memoizedState = hook.baseState = initialState;//hook.memoizedState assignment const queue = (hook.queue = {//Create queue pending: null, dispatch: null, lastRenderedReducer: reducer, lastRenderedState: (initialState: any), }); const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(//Create dispatch function null, currentlyRenderingFiber, queue, ): any)); return [hook.memoizedState, dispatch];//Return memoizedState and dispatch }
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S { return typeof action === 'function' ? action(state) : action; }
-
update phase
When updating, the new state will be calculated according to the update in the hook
function updateReducer<S, I, A>( reducer: (S, A) => S, initialArg: I, init?: I => S, ): [S, Dispatch<A>] { const hook = updateWorkInProgressHook();//Get hook const queue = hook.queue; queue.lastRenderedReducer = reducer; //... the updated state is basically consistent with the state calculation logic in Chapter 12 const dispatch: Dispatch<A> = (queue.dispatch: any); return [hook.memoizedState, dispatch]; }
-
Execution phase
useState will call dispatchAction after executing setState. What dispatchAction does is to add Update to queue.pending, and then start scheduling
function dispatchAction(fiber, queue, action) { var update = {//Create update eventTime: eventTime, lane: lane, suspenseConfig: suspenseConfig, action: action, eagerReducer: null, eagerState: null, next: null }; //Add update to queue.pending var alternate = fiber.alternate; if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) { //If it is an update executed in the render phase, didScheduleRenderPhaseUpdate=true } didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; } else { if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) { //If the fiber does not have priority and the current alternate does not exist or does not have priority, there is no need to update //Optimization steps } scheduleUpdateOnFiber(fiber, lane, eventTime); } }
useEffect
-
statement
Gets and returns the useEffect function
export function useEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null, ): void { const dispatcher = resolveDispatcher(); return dispatcher.useEffect(create, deps); }
-
mount phase
Call mountEffect. mountEffect calls mountEffectImpl. hook.memoizedState is assigned to the effect linked list
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void { const hook = mountWorkInProgressHook();//Get hook const nextDeps = deps === undefined ? null : deps;//rely on currentlyRenderingFiber.flags |= fiberFlags;//Add flag hook.memoizedState = pushEffect(//memoizedState=effects circular linked list HookHasEffect | hookFlags, create, undefined, nextDeps, ); }
-
update phase
Shallow comparison dependency. If the dependency changes, the first parameter of pushEffect is passed to HookHasEffect | hookFlags. HookHasEffect indicates that the useEffect dependency has changed and needs to be re executed in the commit phase
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; let destroy = undefined; if (currentHook !== null) { const prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy;// if (nextDeps !== null) { const prevDeps = prevEffect.deps; if (areHookInputsEqual(nextDeps, prevDeps)) {//Compare deps //Even if the dependencies are equal, add the effect to the linked list to ensure consistent order pushEffect(hookFlags, create, destroy, nextDeps); return; } } } currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushEffect( //The parameter is passed to HookHasEffect | hookFlags. The useEffect containing hookFlags will execute this effect in the commit phase HookHasEffect | hookFlags, create, destroy, nextDeps, ); }
-
Execution phase
In the ninth chapter, the commitLayoutEffects function in the commit stage will call schedulePassiveEffects, destroy the useEffect destructor and callback function push to pendingPassiveHookEffectsUnmount and pendingPassiveHookEffectsMount, then invoke flushPassiveEffects after mutation, execute the destructor function callback of last render and the callback function of this render in turn.
const unmountEffects = pendingPassiveHookEffectsUnmount; pendingPassiveHookEffectsUnmount = []; for (let i = 0; i < unmountEffects.length; i += 2) { const effect = ((unmountEffects[i]: any): HookEffect); const fiber = ((unmountEffects[i + 1]: any): Fiber); const destroy = effect.destroy; effect.destroy = undefined; if (typeof destroy === 'function') { try { destroy();//Destroy function execution } catch (error) { captureCommitPhaseError(fiber, error); } } } const mountEffects = pendingPassiveHookEffectsMount; pendingPassiveHookEffectsMount = []; for (let i = 0; i < mountEffects.length; i += 2) { const effect = ((mountEffects[i]: any): HookEffect); const fiber = ((mountEffects[i + 1]: any): Fiber); try { const create = effect.create;//The creation function of render effect.destroy = create(); } catch (error) { captureCommitPhaseError(fiber, error); } }
useRef
sring type refs are no longer recommended (string s in the source code will generate refs, which occurs in the coerceRef function). ForwardRef just passes refs through parameters. createRef is also a {current: any structure, so we only discuss the useRef of function or {current: any}
//createRef returns {current: any} export function createRef(): RefObject { const refObject = { current: null, }; return refObject; }
-
Declaration phase
Like other hook s
export function useRef<T>(initialValue: T): {|current: T|} { const dispatcher = resolveDispatcher(); return dispatcher.useRef(initialValue); }
-
mount phase
When mount, mountRef is called to create hook and ref objects.
function mountRef<T>(initialValue: T): {|current: T|} { const hook = mountWorkInProgressHook();//Get useRef const ref = {current: initialValue};//ref initialization hook.memoizedState = ref; return ref; }
render stage: add Ref Tag to the Fiber tag with ref attribute, which occurs in markRef in beginWork and completeWork functions
export const Ref = /* */ 0b0000000010000000;
//In beginWork function markRef(current: Fiber | null, workInProgress: Fiber) { const ref = workInProgress.ref; if ( (current === null && ref !== null) || (current !== null && current.ref !== ref) ) { workInProgress.effectTag |= Ref; } } //In completeWork function markRef(workInProgress: Fiber) { workInProgress.effectTag |= Ref; }
commit phase:
Judge whether the ref is changed in the commitMutationEffects function. If it is changed, execute commitDetachRef first, delete the previous ref, and then execute committatchref assignment ref in commitLayoutEffect.
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) { while (nextEffect !== null) { const effectTag = nextEffect.effectTag; // ... if (effectTag & Ref) { const current = nextEffect.alternate; if (current !== null) { commitDetachRef(current);//Remove ref } } }
function commitDetachRef(current: Fiber) { const currentRef = current.ref; if (currentRef !== null) { if (typeof currentRef === 'function') { currentRef(null);//If the type is function, call } else { currentRef.current = null;//Otherwise, assign {current: null} } } }
function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { const instance = finishedWork.stateNode;//Gets an instance of ref let instanceToUse; switch (finishedWork.tag) { case HostComponent: instanceToUse = getPublicInstance(instance); break; default: instanceToUse = instance; } if (typeof ref === 'function') {//ref assignment ref(instanceToUse); } else { ref.current = instanceToUse; } } }
-
update phase
When updating, call update ref to get the current useRef, and then return to the hook linked list
function updateRef<T>(initialValue: T): {|current: T|} { const hook = updateWorkInProgressHook();//Get current useRef return hook.memoizedState;//Return to hook linked list }
useMemo&useCallback
-
Declaration phase
Like other hook s
-
mount phase
The only difference between mount stage useMemo and useCallback is whether callback is stored in memoizedState or the function calculated by callback
function mountMemo<T>( nextCreate: () => T, deps: Array<mixed> | void | null, ): T { const hook = mountWorkInProgressHook();//Create a hook const nextDeps = deps === undefined ? null : deps; const nextValue = nextCreate();//Calculate value hook.memoizedState = [nextValue, nextDeps];//Save value and dependency in memoizedState return nextValue; } function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T { const hook = mountWorkInProgressHook();//Create a hook const nextDeps = deps === undefined ? null : deps; hook.memoizedState = [callback, nextDeps];//Save callback and dependency in memoizedState return callback; }
-
update phase
The same is true for update. The only difference is whether the callback function is used directly or the value returned after the callback is executed is assigned to memoizedState as [?, nextDeps]
function updateMemo<T>( nextCreate: () => T, deps: Array<mixed> | void | null, ): T { const hook = updateWorkInProgressHook();//Get hook const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState !== null) { if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) {//Shallow comparison dependence return prevState[0];//No change, return to the previous state } } } const nextValue = nextCreate();//Call back again if there is a change hook.memoizedState = [nextValue, nextDeps]; return nextValue; } function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T { const hook = updateWorkInProgressHook();//Get hook const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState !== null) { if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) {//Shallow comparison dependence return prevState[0];//No change, return to the previous state } } } hook.memoizedState = [callback, nextDeps];//If changed, assign [callback, nextDeps] to memoizedState again return callback; }
useLayoutEffect
useLayoutEffect is the same as useEffect, except that it is called at different times. It is executed synchronously in the commitLayout function in the commit phase
forwardRef
forwardRef is also very simple, that is, passing the ref attribute
export function forwardRef<Props, ElementType: React$ElementType>( render: (props: Props, ref: React$Ref<ElementType>) => React$Node, ) { const elementType = { $$typeof: REACT_FORWARD_REF_TYPE, render, }; return elementType; } //The second parameter of ForwardRef is the ref object let children = Component(props, secondArg);