This is the 14th day of my participation in the August More Text Challenge. For details, see:August is more challenging
This section is based on the Hook principle (overview) and Hook principle (state Hook) mentioned above, and focuses on the useEffect, useLayoutEffect and other standard side effect hooks.
Create a Hook
In the initial construction stage of fiber, useEffect corresponds to source mountEffect, and useLayoutEffect corresponds to source mountLayoutEffect
mountEffect
:
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null.) :void {
return mountEffectImpl(
UpdateEffect | PassiveEffect, // fiberFlags
HookPassive, // hookFlags
create,
deps,
);
}
Copy the code
mountLayoutEffect
:
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null.) :void {
return mountEffectImpl(
UpdateEffect, // fiberFlags
HookLayout, // hookFlags
create,
deps,
);
}
Copy the code
MountEffect and mountLayoutEffect call mountEffectImpl directly internally, but with different parameters.
mountEffectImpl
:
function mountEffectImpl(fiberFlags, hookFlags, create, deps) :void {
// 1. Create hook
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 2. Set the side effect flag for workInProgress
currentlyRenderingFiber.flags |= fiberFlags; // fiberFlags is marked to workInProgress
Create Effect and mount it on hook.memoizedState
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags, // hookFlags to create effect
create,
undefined,
nextDeps,
);
}
Copy the code
MountEffectImpl logic:
- create
hook
- Set up the
workInProgress
Side effect markers:flags |= fiberFlags
- create
effect
(in thepushEffect
Middle), mount tohook.memoizedState
On, that is,hook.memoizedState = effect
- Note:
State the hooks
In thehook.memoizedState = state
- Note:
Create the Effect
pushEffect:
function pushEffect(tag, create, destroy, deps) {
// 1. Create effect object
const effect: Effect = {
tag,
create,
destroy,
deps,
next: (null: any),
};
// 2. Add the effect object to the end of the ring list
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
/ / new workInProgress updateQueue used to mount the effect object
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
// updateQueue. LastEffect is a circular linked list
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
constfirstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; }}// 3. Return effect
return effect;
}
Copy the code
PushEffect logic:
- create
effect
. - the
effect
Object is added to the end of the circular list. - return
effect
.
Effect’s data structure:
export type Effect = {|
tag: HookFlags,
create: () = > (() = > void) | void.destroy: (() = > void) | void.deps: Array<mixed> | null.next: Effect,
|};
Copy the code
-
Effectie. tag: binary attribute representing the type of effect (source code).
export const NoFlags = / * * / 0b000; export const HasEffect = / * * / 0b001; // There are side effects that can be triggered export const Layout = / * * / 0b010; // Layout is triggered synchronously after dom mutation export const Passive = /* */ 0b100; // Passive: the DOM is triggered asynchronously before the mutation Copy the code
-
Effect.create: This is actually the function passed in via useEffect().
-
Effect. deps: dependencies. If the dependencies change, a new effect is created.
After renderWithHooks are executed, we can draw references to fiber,hook, and effect:
The workinProgress. flags are now marked and will be processed in the commitRoot function during the Fiber tree rendering phase (check back to the fiber tree construction/Fiber tree rendering series).
useEffect & useLayoutEffect
From the perspective of fiber, Hook,effect, you don’t care whether the hook was created using useEffect or useLayoutEffect. All you need to care about is the internal state of fiber.flags,effect.tag.
So the difference between useEffect and useLayoutEffect is as follows:
fiber.flags
different
- use
useEffect
When:fiber.flags = UpdateEffect | PassiveEffect
. - use
useLayoutEffect
When:fiber.flags = UpdateEffect
.
effect.tag
different
- use
useEffect
When:effect.tag = HookHasEffect | HookPassive
. - use
useLayoutEffect
When:effect.tag = HookHasEffect | HookLayout
.
Handle the Effect callback
Once the fiber tree is constructed, the logic enters the render phase. As described in Fiber tree rendering, the entire rendering process is implemented by three functions distributed within the commitRootImpl function:
- commitBeforeMutationEffects
- commitMutationEffects
- commitLayoutEffects
This three functions will deal with fiber. The flags, can also according to circumstance processing fiber. UpdateQueue. LastEffect
commitBeforeMutationEffects
Phase 1: Process the Fiber node with the Passive flag in the side effect queue before dom changes.
function commitBeforeMutationEffects() {
while(nextEffect ! = =null) {
/ /... Omit irrelevant code, only Hook relevant
// Process the 'Passive' flag
const flags = nextEffect.flags;
if((flags & Passive) ! == NoFlags) {if(! rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects =true;
scheduleCallback(NormalSchedulerPriority, () = > {
flushPassiveEffects();
return null; }); } } nextEffect = nextEffect.nextEffect; }}Copy the code
Note: Because flushPassiveEffects are wrapped in a scheduleCallback callback and handled by the scheduling center, and the parameter is NormalSchedulerPriority, So this is an asynchronous callback (see React Scheduler for details).
Because scheduleCallback NormalSchedulerPriority, the callback is asynchronous, flushPassiveEffects will not be executed immediately. Skip the analysis of flushPassiveEffects and continue to commitRoot.
commitMutationEffects
Phase 2: THE DOM changes and the interface is updated.
function commitMutationEffects(root: FiberRoot, renderPriorityLevel: ReactPriorityLevel,) {
/ /... Omit irrelevant code, only Hook relevant
while(nextEffect ! = =null) {
const flags = nextEffect.flags;
const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
case Update: {
/ / useEffect useLayoutEffect will set the Update tag
// Update the node
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break; } } nextEffect = nextEffect.nextEffect; }}function commitWork(current: Fiber | null, finishedWork: Fiber) :void {
/ /... Omit irrelevant code, only Hook relevant
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// Call the destroy function during the mutation phase to ensure that all effect.destroy functions are executed before effect.create
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
return; }}}// Execute the following sequence: effect.destroy
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
constlastEffect = updateQueue ! = =null ? updateQueue.lastEffect : null;
if(lastEffect ! = =null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Filter the effect list according to the tag passed in.
const destroy = effect.destroy;
effect.destroy = undefined;
if(destroy ! = =undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
Copy the code
Call relationship: commitMutationEffects – > commitWork – > commitHookEffectListUnmount.
- Notice that in the call
commitMutationEffects(HookLayout | HookHasEffect, finishedWork)
, the parameter isHookLayout | HookHasEffect
, so only handled byuseLayoutEffect()
To create theeffect
. - According to the analysis above
HookLayout | HookHasEffect
Is through theuseLayoutEffect
To create theeffect
. SocommitMutationEffects
Function can only deal withuseLayoutEffect()
To create theeffect
. - A synchronous invocation
effect.destroy()
.
commitLayoutEffects
Phase 3: After dom changes
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
/ /... Omit irrelevant code, only Hook relevant
while(nextEffect ! = =null) {
const flags = nextEffect.flags;
if (flags & (Update | Callback)) {
/ / useEffect useLayoutEffect will set the Update tag
constcurrent = nextEffect.alternate; commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes); } nextEffect = nextEffect.nextEffect; }}function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
) :void {
/ /... Omit irrelevant code, only Hook relevant
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// Effect. Destroy will never affect effect.create because effect. Destroy has been called before in the commitMutationEffects function
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
schedulePassiveEffects(finishedWork);
return; }}}function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
constlastEffect = updateQueue ! = =null ? updateQueue.lastEffect : null;
if(lastEffect ! = =null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
Copy the code
- Calling relationship:
commitLayoutEffects->commitLayoutEffectOnFiber(commitLifeCycles)->commitHookEffectListMount
.
- Notice that in the call
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork)
, the parameter isHookLayout | HookHasEffect
, so only handled byuseLayoutEffect()
To create theeffect
. - call
effect.create()
Then, the return value is assigned toeffect.destroy
.
-
Prepare for flushPassiveEffects
-
SchedulePassiveEffects (finishedWork) in commitLifeCycles, the finishedWork parameter actually refers to the fiber with side effects that is currently being traversed
-
SchedulePassiveEffects is simple, it filters out the effect with Passive tags (created by useEffect), Add to a global array (pendingPassiveHookEffectsUnmount and pendingPassiveHookEffectsMount).
function schedulePassiveEffects(finishedWork: Fiber) { // 1. Obtain fiber.updatequeue const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); // 2. Obtain the effect ring queue constlastEffect = updateQueue ! = =null ? updateQueue.lastEffect : null; if(lastEffect ! = =null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { const { next, tag } = effect; // 3. Filter out 'effect' created by useEffect() if( (tag & HookPassive) ! == NoHookEffect && (tag & HookHasEffect) ! == NoHookEffect ) {// Add effect to global array and wait for 'flushPassiveEffects' to be processed enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); enqueuePendingPassiveHookEffectMount(finishedWork, effect); } effect = next; } while (effect !== firstEffect); } } export function enqueuePendingPassiveHookEffectUnmount(fiber: Fiber, effect: HookEffect,) :void { // Unmount the Effects array pendingPassiveHookEffectsUnmount.push(effect, fiber); } export function enqueuePendingPassiveHookEffectMount(fiber: Fiber, effect: HookEffect,) :void { // Unmount the Effects array pendingPassiveHookEffectsMount.push(effect, fiber); } Copy the code
-
CommitMutationEffects and commitLayoutEffects2 functions, effect with Layout tag (created by useLayoutEffect), The full callback processing is done (Destroy and CREATE have been called).
The first effect has a Layout tag, so it has effect.destroy(); effect.destroy = effect.create()
flushPassiveEffects
In above commitBeforeMutationEffects stage, asynchronous invocation flushPassiveEffects. During this period with a Passive tag effect has been added to the pendingPassiveHookEffectsUnmount and pendingPassiveHookEffectsMount global array.
FlushPassiveEffects can then access the effects directly from the fiber node
export function flushPassiveEffects() :boolean {
// Returns whether passive effects were flushed.
if(pendingPassiveEffectsRenderPriority ! == NoSchedulerPriority) {const priorityLevel =
pendingPassiveEffectsRenderPriority > NormalSchedulerPriority
? NormalSchedulerPriority
: pendingPassiveEffectsRenderPriority;
pendingPassiveEffectsRenderPriority = NoSchedulerPriority;
// 'runWithPriority' is also an asynchronous call
return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
}
return false;
}
/ /... Omit irrelevant code, only Hook relevant
function flushPassiveEffectsImpl() {
if (rootWithPendingPassiveEffects === null) {
return false;
}
rootWithPendingPassiveEffects = null;
pendingPassiveEffectsLanes = NoLanes;
// 1. Execute effect.destroy()
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') { destroy(); }}// 2. Execute a new effe.create () and assign it to effe.destroy
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); effect.destroy = create(); }}Copy the code
Its core logic:
- traverse
pendingPassiveHookEffectsUnmount
All of theeffect
, the calleffect.destroy()
.- At the same time to empty
pendingPassiveHookEffectsUnmount
- At the same time to empty
- traverse
pendingPassiveHookEffectsMount
All of theeffect
, the calleffect.create()
And updateeffect.destroy
.- At the same time to empty
pendingPassiveHookEffectsMount
- At the same time to empty
Therefore, an effect with Passive tags is fully call-back handled in the flushPassiveEffects function.
Effect with Passive tags executes effect.destroy(); effect.destroy = effect.create()
Update the Hook
Let’s assume that after the first call, the update is initiated and the function is executed again. In this case, the function only uses useEffect, useLayoutEffect and other apis will be executed again.
In the process of update, useEffect corresponds to source updateEffect, and useLayoutEffect corresponds to source updateLayoutEffect. They all call updateEffectImpl internally, just as they were first created, with different arguments.
Update the Effect
updateEffectImpl:
function updateEffectImpl(fiberFlags, hookFlags, create, deps) :void {
// 1. Obtain the current hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
// 2. Analyze dependencies
if(currentHook ! = =null) {
const prevEffect = currentHook.memoizedState;
// Continue using the previous effect.destroy
destroy = prevEffect.destroy;
if(nextDeps ! = =null) {
const prevDeps = prevEffect.deps;
// Compare whether the dependency has changed
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 2.1 Create effect(without HookHasEffect tag) if dependencies remain unchanged
pushEffect(hookFlags, create, destroy, nextDeps);
return; }}}// 2.2 If the dependency changes, change fiber.flag and create effect
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
Copy the code
UpdateEffectImpl with mountEffectImpl logic is different: – if useEffect/useLayoutEffect rely on constant, effect of new objects without HasEffect tag.
Note: The previous effect.destroy is reused regardless of whether the dependency changes. Wait for the call to the commitRoot phase (described above).
The diagram below:
- The first and second ones in the picture
hook
itsdeps
Did not change, reasoneffect.tag
Will not includeHookHasEffect
. - Number 3 in the picture
hook
itsdeps
Change,effect.tag
Continue to containHookHasEffect
.
Handle the Effect callback
After the new hook and the new effect are created, the remaining logic is exactly the same as the first rendering. Effect.destroy and effect.create() are called only if effect.tag contains HookHasEffect
Component destroyed
Were destroyed when the function components, fiber node Deletion marks will inevitably be hit, namely fiber. The flags | = Deletion. The fiber with the Deletion flag is processed at commitMutationEffects:
/ /... Omit irrelevant code
function commitMutationEffects(root: FiberRoot, renderPriorityLevel: ReactPriorityLevel,) {
while(nextEffect ! = =null) {
const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break; }}}}Copy the code
After the commitDeletion function, continue to call unmountHostComponents->commitUnmount. In commitUnmount, execute eff.destroy () to end the closed loop.
conclusion
This section analyzes the whole process of side effect Hook from creation to destruction. In React, accurate identification of effect is realized by relying on fiber.flags and effe.tag. In the commitRoot phase, different types of effect are processed by calling effe.destroy () and then effe.create ().
Write in the last
This article belongs to the diagram react source code in the state management plate, this series of nearly 20 articles, really in order to understand the React source code, and then improve the architecture and coding ability.
The first draft of the graphic section has been completed and will be updated in August. If there are any errors in the article, we will correct them as soon as possible on Github.