Introduction to the
The react update is available in version 17.0.3. You need to have a basic understanding of the react update mechanism before this article. React follows the concept of functional programming, but cannot be used as a function component in actual business due to state and interface calls
Refer to the official documentation for hooks, which have the following advantages:
- Class is difficult to reuse state logic between components. Hooks provide a better native way to share state logic
- Class complex components become difficult to understand because of the lifecycle logic, which hooks break down into smaller units and provide managed side effects like useEffect
- Class is expensive to learn. Hook allows you to use more React features in non-class situations, which is more in line with the concept of function programming
How do Hooks mount
For FunctionComponent types, renderWithHooks are registered with beginWork, and a function is called based on mount or update to generate a hooks list on Fiber
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) { renderLanes = nextRenderLanes; currentlyRenderingFiber$1 = workInProgress; workInProgress.memoizedState = null; workInProgress.updateQueue = null; workInProgress.lanes = NoLanes; // The following should have already been reset { if (current ! == null && current.memoizedState ! == null) { ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV; } else if (hookTypesDev ! == null) { ReactCurrentDispatcher$1.current = HooksDispatcherOnMountWithHookTypesInDEV; } else { ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV; }}... return children; }Copy the code
Hook object properties
hook:{
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
Copy the code
mount
Makes the current point to HooksDispatcherOnMountInDEV, making calls to get the right hooks, object when the update with the mount and approximation, the hooks for linked list is different
HooksDispatcherOnMountInDEV = { ... useRef: function (initialValue) { currentHookNameInDev = 'useRef'; mountHookTypesDev(); return mountRef(initialValue); }, useState: function (initialState) { currentHookNameInDev = 'useState'; mountHookTypesDev(); var prevDispatcher = ReactCurrentDispatcher$1.current; ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV; try { return mountState(initialState); } finally { ReactCurrentDispatcher$1.current = prevDispatcher; }},... unstable_isNewReconciler: enableNewReconciler };Copy the code
Calling hooks in functionComponent creates a hook object that hangs on the current Fiber memoizedState. If any hooks are already dead, next will record the hooks to form a linked list. And record the current hook with workInProgressHook
update
Because of fiber dual caching, both current tree and workInProgress tree have hooks linked to them. If they are empty, they copy the original linked list and use current as a pointer to record the location of the work hook
function updateWorkInProgressHook() {
var nextCurrentHook;
if (currentHook === null) {
var current = currentlyRenderingFiber$1.alternate;
if(current ! = =null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null; }}else {
nextCurrentHook = currentHook.next;
}
var nextWorkInProgressHook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if(nextWorkInProgressHook ! = =null) {
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
if(! (nextCurrentHook ! = =null)) {{throw Error( "Rendered more hooks than during the previous render." );
}
}
currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.workInProgressHook = workInProgressHook.next = newHook; }}return workInProgressHook;
}
Copy the code
Some hooks examples source code parsing
Ready to parse the useEffect | useLayoutEffect with useState;
useEffect | useLayoutEffect
Create the effect
Call pushEffect to create an effect object and hang it on Hook’s memoizedState and in Fiber’s updateQueue to form a circular linked list. There are two useEffect hooks that end up like this: fiber.memoizedState = hook1.next=hook2 hook1.memoizedState = effect1.next=effect2 hook2.memoizedState = Next = Effect1 fiber. UpdateQueue =effect1. Next =effect2 // Different from the updater object stored in class/hostComponent
function pushEffect(tag, create, destroy, deps) {
var effect = {
tag: tag, UseEffect useLayoutEffect is used to distinguish synchronous calls from scheduled calls
create: create, // Callback function, similar to the class component when called componentDidmount/ Update
destroy: destroy, // The return function is executed like the class component's componentWIllUnmount function
deps: deps, / / dependencies
// Circular
next: null // Circular list
};
var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
varfirstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; }}return effect;
}
Copy the code
Update the effect
If the value of the component is not changed, then the pushEffect call is executed. If the value of the component is not changed, then the pushEffect call is executed.
function updateEffectImpl(fiberFlags, hookFlags, create, deps) { var hook = updateWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; var destroy = undefined; if (currentHook ! == null) { var prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy; if (nextDeps ! == null) { var prevDeps = prevEffect.deps; if (areHookInputsEqual(nextDeps, prevDeps)) { hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps); return; } } } currentlyRenderingFiber$1.flags |= fiberFlags; hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps); }Copy the code
Scheduling & Use
The commit function is complex and can be divided into three stages: before mutation, mutation, layout useEffect The stage before mutation is assigned to scheduleCallback for scheduling, and the layout stage is executed. FlushPassiveEffects is executed before mutation because NormalPriority has a lower priority. The previous effect needs to be executed before commit and then scheduled for execution. Inside the flushPassiveEffects method, traverses rootWithPendingPassiveEffects (namely effectList) effect the callback function. EffectList: A linked list collected by the beginWork in the completeWork phase when the Fiber tree is traversed with the Placement Update Deletion label (fiber. flag, formerly called effect) on the Fiber.
function commitRootImpl(root, renderPriorityLevel) {
do {
flushPassiveEffects();
} while(rootWithPendingPassiveEffects ! = =null); .The before mutation stage dispatches flushPassiveEffects in the scheduleCallback
/ / after the layout stage will effectList rootWithPendingPassiveEffects assignment
/ / scheduleCallback trigger flushPassiveEffects, flushPassiveEffects traversal rootWithPendingPassiveEffects inside
if((finishedWork.subtreeFlags & PassiveMask) ! == NoFlags || (finishedWork.flags & PassiveMask) ! == NoFlags) {if(! rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects =true;
scheduleCallback(NormalPriority, function () {
flushPassiveEffects();
return null; }); }}...// Select * from effectList;
// Execute useLayoutEffect destruction for updateEffect funComponent
// Call the componentWillUnmount hook
// Perform the delete operation on deletionEffect and perform the seEffect callback. Remove the refcommitMutationEffects(root, finishedWork, lanes); ./ / layoutEffect execution
commitLayoutEffects(finishedWork, root, lanes);
requestPaint();
}
Copy the code
UseLayoutEffect:
The mutation in the commit of the above function call commitHookEffectListUnmount, role for destruction, to perform updateQueue useLayoutEffect effect on the destroy method
function commitHookEffectListUnmount(flags, finishedWork, nearestMountedAncestor) { var updateQueue = finishedWork.updateQueue; var lastEffect = updateQueue ! == null ? updateQueue.lastEffect : null; if (lastEffect ! == null) { var firstEffect = lastEffect.next; var effect = firstEffect; do { if ((effect.tag & flags) === flags) { // Unmount var destroy = effect.destroy; effect.destroy = undefined; if (destroy ! == undefined) { safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy); } } effect = effect.next; } while (effect ! == firstEffect); }}Copy the code
Next, the commit function executes to the layout stage, commitLayoutEffects sets commitLayoutEffects_begin, With flags (effect) of fiber, perform commitLayoutEffectOnFiber, he actually according to the tag commitHookEffectListMount execution method, The didmount lifecycle is performed when tag equals the class component, updating the ref. CommitHookEffectListMount method invokes the create a new destroy.
function commitHookEffectListMount(tag, finishedWork) { var updateQueue = finishedWork.updateQueue; var lastEffect = updateQueue ! == null ? updateQueue.lastEffect : null; if (lastEffect ! == null) { var firstEffect = lastEffect.next; var effect = firstEffect; do { if ((effect.tag & tag) === tag) { // Mount var create = effect.create; effect.destroy = create(); // Error handling... }}}}Copy the code
UseEffect:
The flushPassiveEffects function executes useEffect, which reduces the priority item in processing effect, changes it back, and then executes flushPassiveEffectsImpl. Because it is scheduled for has been deleted Fiber do processing, finally will also perform the commitHookEffectListUnmount, commitHookEffectListMount
useState|useReducer
The two hooks are basically the same source logically
Custom useMyReducer
Use useState to implement a simple custom useReducer hook
function useMyReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
const dispatch = (action) => {
setState(reducer(state, action))
};
return [state, dispatch]
}
Copy the code
MemoizedState of useState is different from useEffect in that it is memoizedState that generates the update object and hangs it on the queue of hook. Return state and Dispatch
function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
Copy the code
A bit like enqueueSetState, dispatchAction creates an UPDATE list that hangs on queue.pending, gets the priority, calculates whether the state value is equal to the previous one, If there are changes, scheduleUpdateOnFiber is used to enable the execution logic. For useReducer, lastRenderedReducer is the incoming reducer, useState is the state value recorded,
function dispatchAction(fiber, queue, action) { ... Var eventTime = requestEventTime(); var lane = requestUpdateLane(fiber); var update = { lane: lane, action: action, eagerReducer: null, eagerState: null, next: null }; var alternate = fiber.alternate; if (fiber === currentlyRenderingFiber$1 || alternate ! = = null && alternate = = = currentlyRenderingFiber $1) {/ / update reset pointer to form a circular linked list didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; var 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; } else { if (isInterleavedUpdate(fiber)) { var interleaved = queue.interleaved; if (interleaved === null) { update.next = update; pushInterleavedQueue(queue); } else { update.next = interleaved.next; interleaved.next = update; } queue.interleaved = update; } else {var _pending = queue.pending; if (_pending === null) { update.next = update; } else { update.next = _pending.next; _pending.next = update; } queue.pending = update; } if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) { var lastRenderedReducer = queue.lastRenderedReducer; if (lastRenderedReducer ! == null) { var prevDispatcher; { prevDispatcher = ReactCurrentDispatcher$1.current; ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV; } try { var currentState = queue.lastRenderedState; var eagerState = lastRenderedReducer(currentState, action); // Stash the eagerly computed state, and the reducer used to compute update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (objectIs(eagerState, currentState)) { return; } } catch (error) {// Suppress the error. It will throw again in the render phase. } finally { { ReactCurrentDispatcher$1.current = prevDispatcher; }}}}... var root = scheduleUpdateOnFiber(fiber, lane, eventTime); if (isTransitionLane(lane) && root ! == null) { var queueLanes = queue.lanes; queueLanes = intersectLanes(queueLanes, root.pendingLanes); var newQueueLanes = mergeLanes(queueLanes, lane); queue.lanes = newQueueLanes; markRootEntangled(root, newQueueLanes); } } { markStateUpdateScheduled(fiber, lane); }}Copy the code