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:

  1. createhook
  2. Set up theworkInProgressSide effect markers:flags |= fiberFlags
  3. createeffect(in thepushEffectMiddle), mount tohook.memoizedStateOn, that is,hook.memoizedState = effect
    • Note:State the hooksIn thehook.memoizedState = state

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:

  1. createeffect.
  2. theeffectObject is added to the end of the circular list.
  3. returneffect.

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:

  1. fiber.flagsdifferent
  • useuseEffectWhen:fiber.flags = UpdateEffect | PassiveEffect.
  • useuseLayoutEffectWhen:fiber.flags = UpdateEffect.
  1. effect.tagdifferent
  • useuseEffectWhen:effect.tag = HookHasEffect | HookPassive.
  • useuseLayoutEffectWhen: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:

  1. commitBeforeMutationEffects
  2. commitMutationEffects
  3. 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 callcommitMutationEffects(HookLayout | HookHasEffect, finishedWork), the parameter isHookLayout | HookHasEffect, so only handled byuseLayoutEffect()To create theeffect.
  • According to the analysis aboveHookLayout | HookHasEffectIs through theuseLayoutEffectTo create theeffect. SocommitMutationEffectsFunction can only deal withuseLayoutEffect()To create theeffect.
  • A synchronous invocationeffect.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
  1. Calling relationship:commitLayoutEffects->commitLayoutEffectOnFiber(commitLifeCycles)->commitHookEffectListMount.
  • Notice that in the callcommitHookEffectListMount(HookLayout | HookHasEffect, finishedWork), the parameter isHookLayout | HookHasEffect, so only handled byuseLayoutEffect()To create theeffect.
  • calleffect.create()Then, the return value is assigned toeffect.destroy.
  1. 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:

  1. traversependingPassiveHookEffectsUnmountAll of theeffect, the calleffect.destroy().
    • At the same time to emptypendingPassiveHookEffectsUnmount
  2. traversependingPassiveHookEffectsMountAll of theeffect, the calleffect.create()And updateeffect.destroy.
    • At the same time to emptypendingPassiveHookEffectsMount

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 picturehookitsdepsDid not change, reasoneffect.tagWill not includeHookHasEffect.
  • Number 3 in the picturehookitsdepsChange,effect.tagContinue 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.