useEffect of ReactHooks source code analysis

preface

Let me give you an example React.useEffect():

import React, {useEffect} from 'react';
import React from 'react';

export default function App({
  useEffect(()=>{
    console.log('classComponent: componentDidMount')
    return ()=>{
      console.log('classComponent: componentWillUnmount')
    }
  },[])

  return (
    <div>
      a
    </div>


  );
}
Copy code














When executing App(), useEffect(xxx) will be called. Because it is the first call of useEffect(), mounteeffect() in the source code will be executed at this time

1, mountEffect()

effect:

(1) Debugging under dev
(2) Execute mountEffectImpl()

Source code:
//First call React.useEffect Come here
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  if (__DEV__) {
/ / dev code removed
  }
  return mountEffectImpl(
/ / logical or, that is, UpdateEffect+PassiveEffect
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
/ / create is the first parameter of useEffect called
    create,
/ / second optional parameter [] for useEffect
    deps,
  );
}
Copy code

















Resolution:

As you can see, if you don't use dev debugging, just call mountEffectImpl()

2, mountEffectImpl()

effect:

(1) Add the current hook to the workInProgressHook list
(2) Initialize the effect chain and assign to hook.memoizedState

Source code:
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  //Add the current hook to the workInProgressHook list and return the latest hook list
  //For an explanation of mountWorkInProgressHook(), see:
  //[ReactHooks source code parsing useState and why useState should be executed in order]( https://juejin.im/post/5eb7c96ff265da7b90055137 )"I. mountState() parsing (1)" in
  const hook = mountWorkInProgressHook();
  //Initialize deps parameter
  const nextDeps = deps === undefined ? null : deps;
  //Set sideEffectTag to fiberEffectTag (because sideEffectTag=0)
  sideEffectTag |= fiberEffectTag;
  //Initialize the effect chain and return
  //The memoizedState of useEffect hook is not a specific value, but an effect object
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
Copy code












Resolution:

(1) Note the following parameters passed in:
① fiberEffectTag: UpdateEffect | PassiveEffect
② hookEffectTag: UnmountPassive | MountPassive
③ create: the first parameter callback of useEffect, in the example of foreword, namely:


()=>{
    console.log('classComponent: componentDidMount')
    return ()=>{
      console.log('classComponent: componentWillUnmount')
    }
  }
Copy code





④ deps: the second parameter of useEffect depends on the array, in the example: []

(2) Call mountWorkInProgressHook() to add the current hook to the workInProgressHook list and return the latest hook list

const hook = mountWorkInProgressHook();
Copy code

For an explanation of mountWorkInProgressHook(), see:
Usestates of ReactHooks source code parsing and why usestates should be executed in order "I. mountState() parsing (1)" in

(3) Initialize deps parameters

const nextDeps = deps === undefined ? null : deps;
Copy code

(4) Set sideEffectTag to fiberEffectTag

sideEffectTag |= fiberEffectTag;
Copy code

(5) Initializes the effect object and returns

hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
Copy code

useState's hook.memoizedState Is the set value, while useEffect's hook.memoizedState Is an object, that is, an effect object

Let's take a look at what we did in pushEffect

3, pushEffect()

effect:

(1) Initializes the effect object and returns
(2) Add the effect object to the end of the update queue componentUpdateQueue

Source code:
function pushEffect(tag, create, destroy, deps{
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  //Initialize the update queue of FunctionComponent if it does not exist
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    componentUpdateQueue.lastEffect = effect.next = effect;
  }
  //Otherwise, add this effect to the end of the update queue
  else {
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}
Copy code



























Resolution:

(1) Because ReactHooks is a function that provides side effects for FunctionComponent, that is to say, there must be a place to store side effects of FunctionComponent. In the source code, it is the component update queue list to store side effects

(2) If the update queue of FunctionComponent does not exist, createFunctionComponentUpdateQueue() is called to create an update queue, and the effect object of useEffect is placed at the end of the update queue

  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    componentUpdateQueue.lastEffect = effect.next = effect;
  }
Copy code



Add:
Source code of createFunctionComponentUpdateQueue():

//Create update queue for FunctionComponent
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
  return {
    lastEffect: null,
  };
}
Copy code





(3) If the update queue of FunctionComponent exists, add this effect to the end of the update queue

 else {
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
Copy code










In conclusion, when useEffect is called for the first time, React does three things:
① Add the current hook to the workInProgressHook list
② Initialize effect object
③ Add the effect object to the end of the component update queue
④ Assign the effect object to hook.memoizedState



This is done in the render phase, and then the effect will be executed in commit

4, mountEffect() execution time

If you've seen it The first sub stage of Commit "before station" of React source code parsing If "three, commitHookEffectList()" in, you will understand the above effect.create :

effect.create=()=>{
    console.log('classComponent: componentDidMount')
    return ()=>{
      console.log('classComponent: componentWillUnmount')
    }
  }
Copy code





Will execute in commitHookEffectList():

function commitHookEffectList(
  unmountTag: number,
  mountTag: number,
  finishedWork: Fiber,



{
//...
  if ((effect.tag & mountTag) !== NoHookEffect) {
        // Mount
        const create = effect.create;
        effect.destroy = create();
      }
}
Copy code







At this time effect.tag=UnmountPassive | MountPassive:

export const MountPassive = /*         */ 0b01000000//64
export const UnmountPassive = /*       */ 0b10000000//128
Copy code

that is effect.tag=192

Only when the mountTag passed into commithokeffectlist() is MountPassive or UnmountPassive will it be executed effect.create()

When does React call commitHookEffectList() and pass in MountPassive|UnmountPassive?

The call sequence is as follows (both in the commit phase):
commitRootImpl()->flushPassiveEffects()->commitPassiveHookEffects(effect)->commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork)&commitHookEffectList(NoHookEffect, MountPassive, finishedWork)

Add:
For an explanation of commitRootImpl(), see:
Overview of the whole process of commitRoot in React source code analysis

Now let's look at what flushpassive effects () does

5, Flushpassive effects ()

effect:

Eliminating side effects on effect

Core source code:
export function flushPassiveEffects({
  //The first fiber with side effects on the effect list
  /// / for example, in app(), useEffect() is called.
  let effect = root.current.firstEffect;
  while (effect !== null) {
    if (__DEV__) {
      //Removed dev code
    } else {
      try {
        //Perform side effects on fiber
        commitPassiveHookEffects(effect);
      } catch (error) {
        invariant(effect !== null'Should be working on an effect.');
        captureCommitPhaseError(effect, error);
      }
    }
    effect = effect.nextEffect;
  }
}
Copy code


















Resolution:

Side effect on the chain of circular execution effect

6, commitPassiveHookEffects()

effect:

Perform side effects on fiber

Source code:
//Perform side effects on fiber
export function commitPassiveHookEffects(finishedWork: Fiber): void {
  commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
  commitHookEffectList(NoHookEffect, MountPassive, finishedWork);
}
Copy code




analysis

It is mainly to call the commithokeffectlist() function, but pay attention to the following parameters:
① The first call passes UnmountPassive first, then it will execute effect.destory() method corresponds to the development level. When useEffect is called for multiple updates, the return callback function of the previous useEffect will be executed first:

  useEffect(()=>{
    console.log('classComponent: componentDidMount')
    //Execute the callback of this return
    return ()=>{
      console.log('classComponent: componentWillUnmount')
    }
  },[])
Copy code






② The second call passes MountPassive, then it will execute effect.create() method, corresponding to the development level, is to execute the first parameter callback of useEffect:

  useEffect(
    //Execute this callback
    ()=>{
      console.log('classComponent: componentDidMount')
      return ()=>{
        console.log('classComponent: componentWillUnmount')
      }
    },[])
Copy code







This also explains why the call to useEffect will first execute the return callback function of the previous useEffect? This question

7, Commithokeffectlist()

effect:

Loop the effect chain on function component, and perform destroy/create operation (similar to componentDidMount/componentWillUnmount) according to the effect tag on each effect on hooks

Source code:
function commitHookEffectList(
  unmountTag: number,
  mountTag: number,
  finishedWork: Fiber,



{
  //Update queue for FunctionComponent
  //Add: the side-effect of FunctionComponent is placed in the updateQueue.lastEffect On
  //ReactFiberHooks.js The pushEffect() in shows: componentUpdateQueue.lastEffect  =  effect.next  = effect;
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  //If there is side effect, loop the effect chain and execute each effect according to the effect tag
  if (lastEffect !== null) {
    //First side effect
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      //If unmountTag is included, execute destroy() and effect.destroy Set to undefined
      //NoHookEffect is no effect
      if ((effect.tag & unmountTag) !== NoHookEffect) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      //If it contains the effectTag of mountTag, execute create()
      if ((effect.tag & mountTag) !== NoHookEffect) {
        // Mount
        const create = effect.create;
        effect.destroy = create();

        if (__DEV__) {
          //Removed dev code
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
Copy code



































Resolution:

① Mainly refer to previous articles—— The first sub stage of Commit "before station" of React source code parsing "III. commithokeffectlist()"

② Notice how the lastEffect is obtained:

  root.current.firstEffect.updateQueue.lastEffect
Copy code

The latest lastEffect on the update queue of the first effect with side effect in the effect chain of the current fiber node

③ Corresponding to the development level, when App() calls useEffect for the first time, React creates the effect chain of App(), and lastEffect.destory If it is undefined, destory() will not be executed

But it will lastEffect.create(), print out 'classComponent: componentDidMount'

Then, the first time App() calls useEffect, the source code parsing process is over. Next, let's look at the process of calling useEffect multiple times

8, updateEffect()

effect:

Function called when useEffect is called multiple times

Source code:
//When updating many times, go here
function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  if (__DEV__) {
/ / dev code removed
  }
  return updateEffectImpl(
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    create,
    deps,
  );
}
Copy code














Resolution:

Note that the parameters passed by updateEffectImpl() are the same as those passed by mountEffectImpl()

9, updateEffectImpl()

effect:

Compare deps to determine whether the callback of useEffect needs to be executed again

Source code:
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  //hook on the current update's fiber
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;
  //currentHook: hook object on current fiber object
  //When currentHook is not empty
  if (currentHook !== null) {
    //Get old effect status
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    //If the deps parameter exists
    if (nextDeps !== null) {
      //Get old deps parameters
      const prevDeps = prevEffect.deps;
      //Whether deps is the same before and after comparison
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        //If it is the same, it means there is no update, and then the NoHookEffect tag is passed in
        pushEffect(NoHookEffect, create, destroy, nextDeps);
        //return means that the following code is not executed
        return;
      }
    }
  }
  //It can be executed here, indicating that currentHook=null or deps has update
  //Then add UpdateEffectTag
  sideEffectTag |= fiberEffectTag;
  //Initialize the effect chain and return
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
Copy code





























Resolution:

(1) Execute update workinprogresshook() to get the hook on the currently updating fiber

const hook = updateWorkInProgressHook();
Copy code

(2) Get deps for comparison with prevDeps to decide whether to update

const nextDeps = deps === undefined ? null : deps;
Copy code

(3) Then call the core function areHookInputsEqual() to compare whether the deps is the same before and after comparison

if (currentHook !== null) {
    //Get old effect status
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    //If the deps parameter exists
    if (nextDeps !== null) {
      //Get old deps parameters
      const prevDeps = prevEffect.deps;
      //Whether deps is the same before and after comparison
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        //If it is the same, it means there is no update, and then the NoHookEffect tag is passed in
        pushEffect(NoHookEffect, create, destroy, nextDeps);
        //return means that the following code is not executed
        return;
      }
    }
  }
Copy code
















If the result returned by areHookInputsEqual() is true, it means that the effect has no side effect, then add the effect tag of NoHookEffect to the effect to indicate that the callback of useEffect is not updated, and return

(4) areHookInputsEqual() source code

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  //Removed dev code
  if (prevDeps === null) {
    //Removed dev code
    return false;
  }
  //Removed dev code
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}
Copy code

















Because deps is an array, it will cycle to compare each item in the array

be careful:
This is for Object.is() for shallow comparison, that is to say, deep comparison will be updated

(5) Object.is() source code

function is(x,y){
  return ( 
    (x===y&&(x!==0||1/x===1/y)) || (x!==x&&y!==y)
 )
}
Copy code




(6) If areHookInputsEqual() returns false, the following statement is executed

/ / it can be executed here, indicating that currentHook=null or deps has update
/ / then add UpdateEffectTag
  sideEffectTag |= fiberEffectTag;
/ / initialize the effect chain and return
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
Copy code




(7) Then it will call commitPassiveHookEffects() - > commithookeffectlist() again

be careful:
When the same useEffect is called multiple times, the previous destory() will be executed first, and then the current create() will be executed

useEffect flowchart

useEffect flowchart.jpg

GitHub

mountEffect()/mountEffectImpl()/pushEffect()/updateEffect()/updateEffectImpl():
github.com/AttackXiaoJ...

flushPassiveEffects():
github.com/AttackXiaoJ...

commitPassiveHookEffects()/commitHookEffectList():
github.com/AttackXiaoJ...

(end)

Tags: React github

Posted on Sun, 31 May 2020 17:23:00 -0700 by pukstpr12