React source code
9. Hooks: Function Component saves state
Video lessons & Debug demos
The purpose of the video course is to quickly master the process of running the React source code and scheduler, Reconciler, Renderer, Fiber, etc., in react, and debug the source code and analysis in detail to make the process clearer.
Video course: Enter the course
Demos: demo
Course Structure:
- You’re still struggling with the React source code.
- The React Mental Model
- Fiber(I’m dom in memory)
- Start with Legacy or Concurrent (start at the entrance and let’s move on to the future)
- State update process (what’s going on in setState)
- Render phase (awesome, I have the skill to create Fiber)
- Commit phase (I heard renderer marked it for us, let’s map real nodes)
- Diff algorithm (Mom doesn’t worry about my Diff interviews anymore)
- Function Component saves state
- Scheduler&lane model (to see tasks are paused, continued, and queue-jumped)
- What is concurrent mode?
- Handwriting mini React (Short and Sharp is me)
Hook call entry
In hook source code, hook exists in the Dispatcher. Dispatcher is an object. Different hooks call different functions. Global variable ReactCurrentDispatcher. Current according to the mount or update assignment for HooksDispatcherOnMount or HooksDispatcherOnUpdate
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null//mount or update
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
Copy the code
const HooksDispatcherOnMount: Dispatcher = {/ / the mount
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,
/ /...
};
Copy the code
Hook data structure
In FunctionComponent, multiple hooks form a list of hooks that are stored on Fiber’s memoizedState, while updates that need to be updated are stored in hook.queue.pending
const hook: Hook = {
memoizedState: null.// There are different values for different hooks
baseState: null./ / the initial state
baseQueue: null.// Initialize queue
queue: null.// Update that needs to be updated
next: null.// Next hook
};
Copy the code
Now let’s look at the values of memoizedState
- UseState: e.g.
const [state, updateState] = useState(initialState)
.MemoizedState equals
The value of the state - UseReducer: e.g.
const [state, dispatch] = useReducer(reducer, {});
.MemoizedState equals
The value of the state - UseEffect: When mountEffect calls pushEffect to create an effect list,
memoizedState
This is equivalent to the effect list, which is also mounted to fiber.updateQueue. Each effect has the first callback and the second dependency array for useEffect, for example,useEffect(callback, [dep])
, effect is {create:callback, dep:dep… } - UseRef: e.g.
useRef(0)
, memoizedStateIs equal to
{current: 0} - UseMemo: e.g.
useMemo(callback, [dep])
.memoizedState
Is equal to the[callback(), dep]
- UseCallback: e.g.
useCallback(callback, [dep])
.memoizedState
Is equal to the[callback, dep]
.useCallback
savecallback
The function,useMemo
savecallback
Execution result of
useState&useReducer
UseState and useReducer are put together because useState is the useReducer with the default reducer parameters in the source code.
-
UseState&useReducer statement
The resolveDispatcher function retrieves 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); } Copy the code
-
Mount stage
In the mount stage, useState calls mountState and useReducer calls mountReducer. The only difference is that the lastRenderedReducer in the queue created by useState and useReducer are different. 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 = {/ / assignment hook. The queue pending: null.dispatch: null.lastRenderedReducer: basicStateReducer,// mountReducer lastRenderedState: (initialState: any), }); const dispatch: Dispatch<// Create dispatch function BasicStateAction<S>, > = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, quewque, ): 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 the 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 } Copy the code
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S { return typeof action === 'function' ? action(state) : action; } Copy the code
-
The update phase
Update calculates the new state based on update in the hook
function updateReducer<S, I, A>( reducer: (S, A) => S, initialArg: I, init? : I => S, ): [S, Dispatch<A>] { const hook = updateWorkInProgressHook(); Hook const queue = hook. Queue; queue.lastRenderedReducer = reducer; / /... Const dispatch: dispatch <A> = (queue.dispatch: any); return [hook.memoizedState, dispatch]; }Copy the code
-
Execution phase
UseState executes setState and dispatchAction is called. All dispatchAction does is add Update to queue.pending and then dispatch
Function dispatchAction(fiber, queue, action) {var update = { lane, suspenseConfig: suspenseConfig, action: action, eagerReducer: null, eagerState: null, next: null }; Update var alternate = fiber. Pending; if (fiber === currentlyRenderingFiber$1 || alternate ! = = null && alternate = = = currentlyRenderingFiber $1) {/ / if it's render phase perform update didScheduleRenderPhaseUpdate = true} didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; } else { if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) { ScheduleUpdateOnFiber (Fiber, Lane, eventTime);} scheduleUpdateOnFiber(Fiber, Lane, eventTime); }}Copy the code
useEffect
-
The 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); } Copy the code
-
Mount stage
Call mountEffect, mountEffect calls mountEffectImpl, hook. MemoizedState assigns an effect linked list
function mountEffectImpl(fiberFlags, hookFlags, create, deps) :void { const hook = mountWorkInProgressHook();/ / get the hooks const nextDeps = deps === undefined ? null : deps;/ / rely on currentlyRenderingFiber.flags |= fiberFlags;/ / increase the flag hook.memoizedState = pushEffect(//memoizedState= Effects circular list HookHasEffect | hookFlags, create, undefined, nextDeps, ); } Copy the code
-
The update phase
Light is dependent on, if changed pushEffect first parameter dependent preach HookHasEffect | hookFlags, HookHasEffect said useEffect dependencies changed, need to perform 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)) {//比较deps // Add effects to the list even if the dependencies are equal to ensure that the order is consistent pushEffect(hookFlags, create, destroy, nextDeps); return; } } } currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushEffect( / / parameter transmission HookHasEffect | hookFlags containing hookFlags useEffect will perform this effect in the commit phase HookHasEffect | hookFlags, create, destroy, nextDeps, ); } Copy the code
-
Execution phase
SchedulePassiveEffects is called in the commitLayoutEffects function during the Commit phase of Chapter 9. UseEffect destruction and the callback function to push pendingPassiveHookEffectsUnmount and pendingPassiveHookEffectsMount, After mutation, flushPassiveEffects were called to execute the destruction function callback of last render and the callback function of this render successively
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();// The destruction function executes } 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;// Create function of render effect.destroy = create(); } catch(error) { captureCommitPhaseError(fiber, error); }}Copy the code
useRef
Sring refs are no longer recommended. The ForwardRef simply passes refs to the ForwardRef. CreateRef is {current: any} useRef
CreateRef returns {current: any}
export function createRef() :RefObject {
const refObject = {
current: null};return refObject;
}
// the ForwardRef second argument is the ref object
let children = Component(props, secondArg);
Copy the code
-
The statement phase
Just like any other hook
export function useRef<T> (initialValue: T) :{|current: T|} { const dispatcher = resolveDispatcher(); return dispatcher.useRef(initialValue); } Copy the code
-
Mount stage
When mount is called mountRef, the hook and ref objects are created.
function mountRef<T> (initialValue: T) :{|current: T|} { const hook = mountWorkInProgressHook();/ / get useRef const ref = {current: initialValue};/ / ref initialization hook.memoizedState = ref; return ref; } Copy the code
The Render stage: mark Fiber with a ref Tag, which occurs in the beginWork and completeWork functions in one step
export const Ref = / * * / 0b0000000010000000; Copy the code
/ / 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; } } / / completeWork function markRef(workInProgress: Fiber) { workInProgress.effectTag |= Ref; } Copy the code
The commit phase:
If the ref changes, it is checked in the commitMutationEffects function. If the ref changes, the commitDetachRef is first deleted, and then the commitAttachRef is assigned in the 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 the ref}}}Copy the code
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}}}}Copy the code
function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if(ref ! = =null) { const instance = finishedWork.stateNode;// Get 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; }}}Copy the code
-
The update phase
Update call updateRef to retrieve the current useRef and return the hook list
function updateRef<T> (initialValue: T) :{|current: T|} { const hook = updateWorkInProgressHook();// Get the current useRef return hook.memoizedState;// return the list of hooks } Copy the code
useMemo&useCallback
-
The statement phase
Just like any other hook
-
Mount stage
The only difference between useMemo and useCallback in the mount phase is whether callback is stored in memoizedState or the function that callback evaluates
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();/ / the value calculation hook.memoizedState = [nextValue, nextDeps];// Save the value and dependencies 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 dependencies in memoizedState return callback; } Copy the code
-
The update phase
Update is the same, the only difference is whether the callback function is assigned to memoizedState directly or the value returned after the callback is executed as [?, nextDeps]
function updateMemo<T> (
nextCreate: () => T,
deps: Array<mixed> | void | null.) :T {
const hook = updateWorkInProgressHook();/ / get the hooks
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 dependencies
return prevState[0];// Return to the same state}}}const nextValue = nextCreate();// Call callback again if changes occur
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function updateCallback<T> (callback: T, deps: Array<mixed> | void | null) :T {
const hook = updateWorkInProgressHook();/ / get the hooks
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 dependencies
return prevState[0];// Return to the same state
}
}
}
hook.memoizedState = [callback, nextDeps];// Reassign [callback, nextDeps] to memoizedState
return callback;
}
Copy the code