preface

In this section, we will analyze useEffect and useLayoutEffect from the perspective of source code. You will learn:

  • The implementation principle of useEffect and useLayoutEffect;
  • UseEffect and useLayoutEffect call timing and execution mode respectively.

Let’s explore the following example:

import React, { useState, useEffect, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom';

function App() {
    const [count, setCount] = useState(0);
    useEffect(() = > {
        console.log('Execute effect callback');
        return () = > {
            console.log('Execute effect destruction function')}}, []); useLayoutEffect(() = > {
        console.log('Execute layoutEffect callback');
        return () = > {
            console.log('Execute layoutEffect destruction function')}});return (
        <div onClick={()= > setCount(count + 1)}>Hello Effect</div>
    )
}

ReactDOM.render(<App />.window.root, function () { console.log(this)});Copy the code

UseEffect and useLayoutEffect in the source code

React provides the definition of the API

function resolveDispatcher() {
    var dispatcher = ReactCurrentDispatcher.current;
    return dispatcher;
}

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

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

As you can see, these two calls effect ReactCurrentDispatcher. The current provided in the method, and useState, useReducer internal implementation, there is the mount and the update of two different stages.

Function component mount effect Hook implementation

export const UpdateEffect = / * * / 0b000000000000100; / / 4
export const PassiveEffect = / * * / 0b000001000000000; / / 512
export const HookHasEffect = / * * / 0b001; / / 1
export const HookLayout = / * * / 0b010; / / 2
export const HookPassive = /*   */ 0b100; / / 4

function mountEffect(create, deps) {
    return mountEffectImpl(UpdateEffect | PassiveEffect, HookPassive, create, deps);
}

function mountLayoutEffect(create, deps) {
    return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
}

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
    const hook = mountWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    currentlyRenderingFiber.effectTag |= fiberEffectTag; // --> 2 |= 516  /  2 |= 4
    hook.memoizedState = pushEffect(HookHasEffect | hookEffectTag, create, undefined, nextDeps);
}
Copy the code

Both effects call mountEffectImpl.

If a function component has an Effect hook, the function component will be marked with an effectTag. If the function component has an Effect hook, the function component will be marked with an effectTag. During the COMMIT phase, Effet hooks on components are executed based on effectTags. Finally return to hook.memoizedState via pushEffect.

For useState, it’s hook. MemoizedState is its state, but Effect Hook’s memoizedState is slightly different.

function pushEffect(tag, create, destroy, deps) {
    // create effect node
    const effect = {
        tag,
        create,
        destroy,
        deps,
        next: null};// Add effect to updateQueue of component Fiber
    let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
    if (componentUpdateQueue === null) { // The first Effect hits here
        componentUpdateQueue = createFunctionComponentUpdateQueue();
        currentlyRenderingFiber.updateQueue = componentUpdateQueue;
        componentUpdateQueue.lastEffect = effect.next = effect;
    } else { // The subsequent Effect will hit here
        const lastEffect = componentUpdateQueue.lastEffect;
        if (lastEffect === null) {
            componentUpdateQueue.lastEffect = effect.next = effect;
        } else {
            // Add the current useEffect update to the update queue
            constfirstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; }}// return to the effect node
    return effect;
}

function createFunctionComponentUpdateQueue() {
    return { lastEffect: null }; // The function component forms the updateQueue through a lastEffect property
}
Copy the code

In pushEffect an effect data structure is created, then effect is added to the function component fiber’s updateQueue updateQueue, and this created effect is returned as hook.memoizedstate. The effect data structure holds the effect callbacks, dependencies, and its destruction function.

Effect Hook implementation when function component update

function updateEffect(create, deps) {
    return updateEffectImpl(UpdateEffect | PassiveEffect, HookPassive, create, deps);
}

function updateLayoutEffect(create, deps) {
    return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
    const hook = updateWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    let destroy = undefined;

    if(currentHook ! = =null) {
        const prevEffect = currentHook.memoizedState;
        // Get the destruction function returned after the last Effect callback (if any).
        destroy = prevEffect.destroy;
        If the dependency is equal, no need to trigger the callback again. In this case, call pushEffect to create an effect and add it to fiber.updateQueue
        if(nextDeps ! = =null) {
            const prevDeps = prevEffect.deps;
            if (areHookInputsEqual(nextDeps, prevDeps)) {
                pushEffect(hookEffectTag, create, destroy, nextDeps);
                return; }}}// if the dependencies are not equal, mark effectTag and call pushEffect to create effect as hook.memoizedState
    currentlyRenderingFiber.effectTag |= fiberEffectTag;
    hook.memoizedState = pushEffect(
        HookHasEffect | hookEffectTag,
        create,
        destroy,
        nextDeps,
    );
}

function areHookInputsEqual(nextDeps, prevDeps) {
    if (prevDeps === null) {
        return false;
    }
    for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
        if (is(nextDeps[i], prevDeps[i])) { // object. is compares whether two values are equal
            continue;
        }
        return false;
    }
    return true;
}
Copy the code

Get the previous hook first in updateEffectImpl; We then get the Distory destruction function to save to the new effect; If the dependency has not changed, do not set the update flag on the component Fiber node; If the dependency changes, set the update flag on the component Fiber.

Either update or not, pushEffect is called to recreate an effect and add it to the updateQueue of the component Fiber.

So what’s the difference? The difference is the tag passed when pushEffect is called. If you want to update, to the effect of the tag is HookHasEffect | hookEffectTag, no update, just hookEffectTag, missing HookHasEffect, The execution conditions for the Effect callback are not met when called in the COMMIT phase.

See useEffect, useLayoutEffect call timing from the source point of view

Above we know the implementation principle of these two effects. In fact, in the Render phase of the component, the Effect corresponding to hook is bound to the updateQueue updateQueue of the component Fiber, and the component is marked with effectTag. When are the two Effect Hook callbacks and destructors called and executed?

In the COMMIT phase, calls are made to the callbacks of both hooks. Since the COMMIT stage is divided into three sub-stages: before Mutation, Mutation and Layout, all of which involve Effect hook processing, we will explore these two sub-stages as follows.

BeforeMutation child stage Effect Hook processing

To be exact, this little phase is only handled for useEffect in the Hooks.

We know that in the completeWork phase of the Render phase, the Fiber nodes that need to perform side effects form an effectList with side effects wrapped:

  • Insert DOM node (Placement);
  • DOM node Update (Update);
  • Delete a DOM node.

In addition, when a FunctionComponent contains useEffect or useLayoutEffect, the Fiber node corresponding to the FunctionComponent is also assigned an effectTag.

However, note the difference between the two Effect hooks when assigning an Effect Tag: UseLayoutEffect uses HookLayout = 2 and useEffect PassiveEffect = 512 as its effectTag.

BeforeMutation son under the commit phase, will call commitBeforeMutationEffects function to handle effectList list, one of the judgment conditions, If the effectTag value is 512 and the effectTag type is useEffect, useEffect hooks exist on the function component:

function commitBeforeMutationEffects() {
    while(nextEffect ! = =null) {
        // ...
        const effectTag = nextEffect.effectTag;
        if((effectTag & Passive) ! == NoEffect) {// PassiveEffect = Passive = 512
            if(! rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects =true;
                scheduleCallback(NormalSchedulerPriority, () = > {
                    / / triggers useEffect
                    flushPassiveEffects();
                    return null; }); } } nextEffect = nextEffect.nextEffect; }}Copy the code

Focus on one variable first: RootDoesHavePassiveEffects, the initial value is false, there will be value to true, this variable will be behind Layout substages is completed, Used to save the root to the global variable rootWithPendingPassiveEffects (with code below);

Then you see a method: ScheduleCallback, provided by the Scheduler module, is used to asynchronously schedule a callback function with a priority. In this case, the asynchronously scheduled callback is the flushPassiveEffects method that triggers the useEffect callback.

Here are just a few important clues to focus on:

  • UseEffect callback function and destruction function are asynchronously scheduled through scheduleCallback. The function used for scheduling is flushPassiveEffects, and the timing of asynchronous execution of the scheduling function will be operated after the completion of the commit phase.
  • The son in the Layout phase is completed, will save the root to global variables rootWithPendingPassiveEffects, the purpose is to in flushPassiveEffects asynchronous execution scheduling function, After the useEffect callback is executed, it is used to clear the effectList of side effects (initialization).
  • In flushPassiveEffects, useEffect is used to trigger the destroy function and render execution function. This problem is introduced below.

After the Layout sub-stage is complete, store root (effectList) :

function commitRootImpl(root, renderPriorityLevel) {
    / /... BeforeMutation, Mutation, Layout phase logic
    
    const rootDidHavePassiveEffects = rootDoesHavePassiveEffects; // Effect is attached to the root node
    / / a: useEffect callback function untreated, save the root to the rootWithPendingPassiveEffects,
    FlushPassiveEffect removes the effectList after useEffect destruction and callback.
    if (rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = false;
        rootWithPendingPassiveEffects = root; // There is useEffect to handle, save root
        pendingPassiveEffectsLanes = lanes;
        pendingPassiveEffectsRenderPriority = renderPriorityLevel;
    } else {
        // No useEffect is left, that is, all effectList side effects have been processed
        / /... Initialize the effectList to NULL}}Copy the code

Mutation stage processing Effect

To be exact, this little phase only applies to the useLayoutEffect handle in Hooks.

In the Mutation stage, element nodes are processed according to effectTag type. In the case of a function component, this is where the useLayoutEffect hook destruction function is executed when the Update effectTag is marked.

Until now, I thought useEffect and useLayoutEffect would execute their destruction functions only when the component is uninstalled. Today, HOWEVER, I discovered that the destruction function is also executed on every update rendering, but its execution timing takes precedence over the callback function.

If you’re used to using class, you might be wondering why the Effect cleanup phase is performed every time you re-render, rather than just once when you uninstall a component. Let’s take a look at a practical example of why this design can help us create less buggy components. (See React for an example.)

In the Mutation phase, commitMutationEffects are called, or in the case of Update, commitWork is called:

function commitMutationEffects(root, renderPriorityLevel) {
    while(nextEffect ! = =null) {
        const effectTag = nextEffect.effectTag;
        // ...
        // This is where the bitwise operator benefits. It is very quick to determine which conditions are currently satisfied
        // (mainly due to the different bits of each condition, they have rules: 2, 4, 8... To ensure that each digit is not repeated)
        const primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating);
        switch (primaryEffectTag) {
            // ...
            / / update the DOM
            case Update: {
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
            }
            / / remove the DOM
            case Deletion: {
                commitDeletion(root, nextEffect, renderPriorityLevel);
                break; } } nextEffect = nextEffect.nextEffect; }}Copy the code

Mainly in commitWork call commitHookEffectListUnmount to perform useLayoutEffect hook destructor:

function commitWork(current, finishedWork) {
    switch (finishedWork.tag) {
        case FunctionComponent:
        case ForwardRef:
        case MemoComponent:
        case SimpleMemoComponent:
        case Block: {
            commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
            return;
        }
        // ...
}

function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
    const updateQueue = finishedWork.updateQueue;
    constlastEffect = updateQueue ! = =null ? updateQueue.lastEffect : null;
    if(lastEffect ! = =null) {
        const firstEffect = lastEffect.next;
        let effect = firstEffect;
        do {
            if ((effect.tag & tag) === tag) {
                // Unmount
                const destroy = effect.destroy;
                effect.destroy = undefined; // Each time the destruction function is executed, it is cleared
                if(destroy ! = =undefined) {
                    destroy();
                }
            }
            effect = effect.next;
        } while (effect !== firstEffect);
    }
}
Copy the code

UseEffectLayout is used to destroy a component Update. If the component is Deletion effectTag, it goes to the commitUnmount method, and eventually the commitUnmount method is called to execute the deStory destruction function. UseEffect and useLayoutEffect are also distinguished:

function commitUnmount(finishedRoot, current, renderPriorityLevel) {
    switch (current.tag) {
    case FunctionComponent: {
        const updateQueue = current.updateQueue;
        if(updateQueue ! = =null) {
            const lastEffect = updateQueue.lastEffect;
            if(lastEffect ! = =null) {
                const firstEffect = lastEffect.next;
                let effect = firstEffect;
                do {
                    const { destroy, tag } = effect;
                    if(destroy ! = =undefined) {
                        if((tag & HookPassive) ! == NoHookEffect) {// useEffect
                            enqueuePendingPassiveHookEffectUnmount(current, effect);
                        } else { // useLayoutEffect
                            destroy();
                        }
                    }
                    effect = effect.next;
                } while (effect !== firstEffect);
            }
        }
        return; }}Copy the code

Effects handled by the Layout child

UseEffect and useLayoutEffect are both used for Hook processing.

In this stage, we call the commitLayoutEffects method to traverse the effectList and process effects in turn. When the effectTag type meets Update (UpdateEffect = 4), Trigger hooks commitLayoutEffectOnFiber method to invoke the life cycle (class components) and Hook related operations.

function commitLayoutEffects(root, committedLanes) {
    while(nextEffect ! = =null) {
        const effectTag = nextEffect.effectTag;
        // Call lifecycle hooks and hooks
        if (effectTag & (Update | Callback)) {
            constcurrent = nextEffect.alternate; commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes); } nextEffect = nextEffect.nextEffect; }}Copy the code

CommitLayoutEffectOnFiber method is actually an alias commitLifeCycles method, can according to different types of nodes within the method for processing, if is FunctionComponent, are:

  • Execute the useLayoutEffect hook callback;
  • Schedule the useEffect destruction and callback functions.
function commitLifeCycles(finishedRoot, current, finishedWork, committedLanes) {
    switch (finishedWork.tag) {
        case FunctionComponent:
        case ForwardRef:
        case SimpleMemoComponent:
        case Block: {
            // Execute the useLayoutEffect callback function
            commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
            // Schedule useEffect destruction and callback functions
            schedulePassiveEffects(finishedWork);
            return;
        }
        // ...
}
Copy the code

For useLayoutEffect, the callback function is executed in commitHookEffectListMount, as you can see, the first parameter is the tag, the pass is HookLayout here, That is, it will only execute the useLayoutEffect callback, not useEffect callback, as follows:

function commitHookEffectListMount(tag, finishedWork) {
    const updateQueue = finishedWork.updateQueue; // Get Effect on the function component
    constlastEffect = updateQueue ! = =null ? updateQueue.lastEffect : null;
    if(lastEffect ! = =null) {
        const firstEffect = lastEffect.next;
        let effect = firstEffect;
        do {
            if ((effect.tag & tag) === tag) { // Satisfy LayoutEffect to execute callback function
                const create = effect.create;
                effect.destroy = create();
            }
            effect = effect.next;
        } while (effect !== firstEffect);
    }
}
Copy the code

As you can see, the useLayoutEffect callback is executed synchronously after entering the commit-Layout phase for both initial and updated renderings.

UseEffect The actual timing of the destruction and callback functions

By now we know when useLayoutEffect is implemented. However, the call timing of useEffect is still mysterious. It participates in asynchronous scheduling in the before Mutation and Layout phases. What are the execution timing of its destruction function and callback function?

The useLayoutEffect callback, which we discussed in the Layout stage above, is not covered: inside the method schedulePassiveEffecs saves useEffect’s effect into two global variables: PendingPassiveHookEffectsMount and pendingPassiveHookEffectsUnmount.

function schedulePassiveEffects(finishedWork: Fiber) {
    const updateQueue = finishedWork.updateQueue;
    constlastEffect = updateQueue ! = =null ? updateQueue.lastEffect : null;
    if(lastEffect ! = =null) {
        const firstEffect = lastEffect.next;
        let effect = firstEffect;
        do {
            const { next, tag } = effect;
            if ( UseEffect (not useLayoutEffect) and the callback needs to be executed (HookHasEffect exists)(tag & HookPassive) ! == NoHookEffect && (tag & HookHasEffect) ! == NoHookEffect ) { enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); enqueuePendingPassiveHookEffectMount(finishedWork, effect); } effect = next; }while (effect !== firstEffect);
    }
}

function enqueuePendingPassiveHookEffectUnmount(fiber, effect) {
    pendingPassiveHookEffectsUnmount.push(effect, fiber); // Add both Effect and fiber
}

function enqueuePendingPassiveHookEffectMount(fiber, effect) {
    pendingPassiveHookEffectsMount.push(effect, fiber); // Add both Effect and fiber
}
Copy the code

When we discussed the before Mutation stage above, we said that useEffect would be asynchronously scheduled by scheduleCallback. When the execution time reaches, the callback function called by scheduling is flushPassiveEffects, and the priority will be set inside this method. And executes the flushPassiveEffectsImpl method. For flushPassiveEffectsImpl, it mainly does three things:

  • Call the useEffect destruction function;
  • Call the useEffect callback;
  • According to the rootWithPendingPassiveEffects remove effectList (root).

The core code in flushPassiveEffectsImpl is posted below and then analyzed step by step:

function flushPassiveEffectsImpl() {
    // Step 0: Determine whether there are executable work tasks
    if (rootWithPendingPassiveEffects === null) {
        return false;
    }

    const root = rootWithPendingPassiveEffects;
    rootWithPendingPassiveEffects = null;

    // First pass: Destroy stale passive effects.
    const unmountEffects = pendingPassiveHookEffectsUnmount;
    pendingPassiveHookEffectsUnmount = [];
    for (let i = 0; i < unmountEffects.length; i += 2) {
        const effect = unmountEffects[i];
        const fiber = unmountEffects[i + 1];
        const destroy = effect.destroy;
        effect.destroy = undefined;

        if (typeof destroy === 'function') { destroy(); }}// Second pass: Create new passive effects.
    const mountEffects = pendingPassiveHookEffectsMount;
    pendingPassiveHookEffectsMount = [];
    for (let i = 0; i < mountEffects.length; i += 2) {
        const effect = mountEffects[i];
        const fiber = mountEffects[i + 1];

        const create = effect.create;
        effect.destroy = create();
    }

    / / remove effectList
    let effect = root.current.firstEffect;
    while(effect ! = =null) {
        const nextNextEffect = effect.nextEffect;
        effect.nextEffect = null;
        if(effect.effectTag & Deletion) { detachFiberAfterEffects(effect); } effect = nextNextEffect; }}Copy the code

Key role: rootWithPendingPassiveEffects global variables

After the Layout subphase is complete, if there is no useEffect to execute, clear the effectList directly. If have useEffect to perform, do not remove effectList, but the root (the above entities effectList) saved to a global variable rootWithPendingPassiveEffects.

When flushPassiveEffectsImpl is entered, there will be a judgment condition. If this variable has a value, it indicates that there is useEffect to be executed and effectList to be cleared, and corresponding processing will be carried out.

UseEffect Destroys the execution of the function

In this step, you will be obtained from the global variable pendingPassiveHookEffectsUnmount need to destroy the effect of a function, and destroy the function effect when it is added to this global variable, is above do we say that the Layout of phase, Here we can see that for I, it is effect, and I + 1 is the corresponding fiber. The reason why the fiber node is saved is for some exception handling (the code is simplified here, so it is deleted).

UseEffect Execution of the callback function

In this step, you will be obtained from the global variable pendingPassiveHookEffectsMount needs to execute callback function effect, and the callback function when the effect is added to this global variable, is above do we say that the Layout of phase.

Remove effectLis

At this point, all effectLists have been executed, and the effectList is cleared for initialization.

So, the whole useEffect asynchronous call is divided into four parts:

  • In the beforeMutation stage, scheduleCallback is called and flustPassiveEffects is used as callback to start a request for asynchronous scheduling useEffect.
  • In the layout stage will need to perform useEffect to join the two global variable: pendingPassiveHookEffectsUnmount and pendingPassiveHookEffectsMount;
  • After the layout stage, will save root (effectList) to global variables rootWithPendingPassiveEffects;
  • When scheduleCallback executes Flow passiveEffects, the useEffect destruction function and useEffect callback function are executed.

conclusion

  1. For useLayoutEffect hooks, it is executed synchronously.
  • In the commit-mutation phase, all destruction functions of useLayoutEffect hook are iterated and executed (update rendering phase).
  • All useLayoutEffect hook callbacks are iterated during the commit-layout phase (initial render and update render phase, which is executed only if the dependency is not the same).
  1. For useEffect hook, it is executed asynchronously.
  • In the commit-beforemutation stage, scheduleCallback is called to flustPassiveEffects as a callback, and a request for asynchronous scheduling useEffect is started.
  • In a commit – layout stage will need to perform useEffect to join the two global variable: pendingPassiveHookEffectsUnmount and pendingPassiveHookEffectsMount;
  • After a commit – layout stage, will save root (effectList) to global variables rootWithPendingPassiveEffects;
  • When scheduleCallback executes Flow passiveEffects, the useEffect destruction function and useEffect callback function are executed.
  1. About the destruction function.

We have seen above that the destruction function, like the callback function, executes every update. How do we implement the unload function that executes only once in the class lifecycle? The answer is that if the second argument to useEffect is an empty array, you can make this useEffect execute the callback and destruction function only once.