ReactHooks源码解析之useEffect

3,327 阅读13分钟

前言

举个例子来讲解下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>

  );
}

当执行App()时,会调用useEffect(xxx),因为是useEffect()的第一次调用,所以此时会执行源码里的mountEffect()

一、mountEffect()

作用:

(1) 在 dev 下调试
(2) 执行mountEffectImpl()

源码:
//首次调用 React.useEffect 走这里
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  if (__DEV__) {
    //删除了 dev 代码
  }
  return mountEffectImpl(
    //逻辑或,即 是 UpdateEffect+PassiveEffect
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    //create 也就是 useEffect的第一个参数 callback
    create,
    //useEffect 的第二个可选参数 []
    deps,
  );
}
解析:

可以看到,如果不用 dev 调试的话,直接调mountEffectImpl()就可以了

二、mountEffectImpl()

作用:

(1) 将当前hook加入workInProgressHook链表中
(2) 初始化effect链并赋值给hook.memoizedState

源码:
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  //将当前 hook 加入 workInProgressHook 链表中,并返回最新的 hook 链表
  //关于mountWorkInProgressHook()的讲解,请看:
  //[ReactHooks源码解析之useState及为什么useState要按顺序执行](https://juejin.cn/post/6844904152712085512)中的「一、mountState()解析(1)」
  const hook = mountWorkInProgressHook();
  //初始化 deps 参数
  const nextDeps = deps === undefined ? null : deps;
  //将 sideEffectTag 置为 fiberEffectTag(因为sideEffectTag=0)
  sideEffectTag |= fiberEffectTag;
  //初始化 effect 链并返回
  //useEffect hook 的 memoizedState 并不是一个具体的值,而是一个 effect 对象
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
解析:

(1) 注意下传入的参数:
① fiberEffectTag:UpdateEffect | PassiveEffect
② hookEffectTag:UnmountPassive | MountPassive
③ create:useEffect 的第一个参数 callback,在「前言」的例子中,也就是:

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

④ deps:useEffect 的第二个参数 依赖数组,在例子中是:[ ]

(2) 调用mountWorkInProgressHook(),将当前hook加入workInProgressHook链表中,并返回最新的hook链表

const hook = mountWorkInProgressHook();

关于mountWorkInProgressHook()的讲解,请看:
ReactHooks源码解析之useState及为什么useState要按顺序执行中的 一、mountState()解析(1)

(3) 初始化deps参数

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

(4) 将 sideEffectTag 置为 fiberEffectTag

sideEffectTag |= fiberEffectTag;

(5) 初始化 effect 对象并返回

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

useState的hook.memoizedState是设置的值,而 useEffect 的hook.memoizedState是一个对象,也就是 effect 对象

接下来我们看下pushEffect里做了什么

三、pushEffect()

作用:

(1) 初始化effect对象并返回
(2) 将effect对象添加至更新队列componentUpdateQueue末尾

源码:
function pushEffect(tag, create, destroy, deps{
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  //如果 FunctionComponent 的更新队列不存在的话,则初始化它
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    componentUpdateQueue.lastEffect = effect.next = effect;
  }
  //否则将此 effect 添加至更新队列末尾
  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;
}
解析:

(1) 因为 ReactHooks 是给 FunctionComponent 提供副作用的函数,也就是说一定是有一个地方来存放 FunctionComponent 的副作用的,那么在源码里就是componentUpdateQueue链表来存放副作用的

(2) 如果FunctionComponent的更新队列不存在,则调用createFunctionComponentUpdateQueue()来创建一个更新队列,并将该useEffecteffect对象放至更新队列队尾

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

补充:
createFunctionComponentUpdateQueue()的源码:

//创建 FunctionComponent 的更新队列
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
  return {
    lastEffectnull,
  };
}

(3) 如果FunctionComponent的更新队列存在的话,则将此effect添加至更新队列末尾

 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;
    }
  }

综上,可以看到当第一次调用useEffect时,React 做了 3 件事:
① 将当前hook加入workInProgressHook链表中
② 初始化effect对象
③ 将effect对象加入componentUpdateQueue更新队列(FunctionComponent存放副作用的链表)队尾
④ 将effect对象赋值给hook.memoizedState

以上是在render阶段完成的,接下来会在commit执行该effect

四、mountEffect()执行的时机

如果你看过中的React源码解析之Commit第一子阶段「before mutation」中的「三、commitHookEffectList()」的话,那么就会明白上文的effect.create

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

会在commitHookEffectList()中执行:

function commitHookEffectList(
  unmountTag: number,
  mountTag: number,
  finishedWork: Fiber,
{
//...
  if ((effect.tag & mountTag) !== NoHookEffect) {
        // Mount
        const create = effect.create;
        effect.destroy = create();
      }
}

此时的effect.tag=UnmountPassive | MountPassive

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

也就是effect.tag=192

只有当传进commitHookEffectList()mountTagMountPassive或者是UnmountPassive,才会执行effect.create()

那么React是在什么时候调用commitHookEffectList()时传入MountPassive|UnmountPassive呢?

调用顺序如下(均在 commit 阶段):
commitRootImpl()——>flushPassiveEffects()——>commitPassiveHookEffects(effect)——>commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork)&commitHookEffectList(NoHookEffect, MountPassive, finishedWork)

补充:
关于commitRootImpl()的讲解,请看:
React源码解析之commitRoot整体流程概览

接下来我们看下flushPassiveEffects()主要做了什么

五、flushPassiveEffects()

作用:

清除effect上的副作用

核心源码:
export function flushPassiveEffects({
  //effect 链表上第一个有副作用的 fiber
  ////比如在 app() 中调用了 useEffect()
  let effect = root.current.firstEffect;
  while (effect !== null) {
    if (__DEV__) {
      //删除了 dev 代码
    } else {
      try {
        //执行 fiber 上的副作用
        commitPassiveHookEffects(effect);
      } catch (error) {
        invariant(effect !== null'Should be working on an effect.');
        captureCommitPhaseError(effect, error);
      }
    }
    effect = effect.nextEffect;
  }
}
解析:

循环执行 effect 链上的副作用(side-effect

六、commitPassiveHookEffects()

作用:

执行 fiber 上的副作用

源码:
//执行 fiber 上的副作用
export function commitPassiveHookEffects(finishedWork: Fiber): void {
  commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
  commitHookEffectList(NoHookEffect, MountPassive, finishedWork);
}
解析

主要是调用commitHookEffectList()函数,但要注意下传的参数:
① 第一次调用先传的是UnmountPassive,那么就会执行effect.destory()方法,对应到开发层面,就是当多次更新调用useEffect时,会先执行上个useEffectreturn回调函数:

  useEffect(()=>{
    console.log('classComponent:componentDidMount')
    //执行这个 return 的 callback
    return ()=>{
      console.log('classComponent:componentWillUnmount')
    }
  },[])

② 第二次调用传的是MountPassive,那么就会执行effect.create()方法,对应到开发层面, 就是执行useEffect的第一个参数callback

  useEffect(
    //执行这个 callback
    ()=>{
      console.log('classComponent:componentDidMount')
      return ()=>{
        console.log('classComponent:componentWillUnmount')
      }
    },[])

这也就解释了调用useEffect为什么会先执行上个useEffect的return回调函数? 这个问题

七、commitHookEffectList()

作用:

循环FunctionComponent上的effect链,根据hooks上每个effect上的 effectTag,执行destroy/create操作(类似于componentDidMount/componentWillUnmount

源码:
function commitHookEffectList(
  unmountTag: number,
  mountTag: number,
  finishedWork: Fiber,
{
  //FunctionComponent 的更新队列
  //补充:FunctionComponent的 side-effect 是放在 updateQueue.lastEffect 上的
  //ReactFiberHooks.js中的pushEffect()里有说明: componentUpdateQueue.lastEffect = effect.next = effect;
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  //如果有副作用 side-effect的话,循环effect 链,根据 effectTag,执行每个 effect
  if (lastEffect !== null) {
    //第一个副作用
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      //如果包含 unmountTag 这个 effectTag的话,执行destroy(),并将effect.destroy置为 undefined
      //NoHookEffect即NoEffect
      if ((effect.tag & unmountTag) !== NoHookEffect) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      //如果包含 mountTag 这个 effectTag 的话,执行 create()
      if ((effect.tag & mountTag) !== NoHookEffect) {
        // Mount
        const create = effect.create;
        effect.destroy = create();

        if (__DEV__) {
          //删除了 dev 代码
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
解析:

① 主要参考之前写的文章——React源码解析之Commit第一子阶段「before mutation」 三、commitHookEffectList()

② 注意下lastEffect是怎么取到的:

  root.current.firstEffect.updateQueue.lastEffect

当前 fiber 节点上的 effect 链上第一个有 side-effect 的 effect 的更新队列上的最新的 lastEffect

③ 对应到开发层面上,当 App() 第一次调用useEffect时,React 创建 App() 的 effect 链,并将lastEffect.destory赋为undefined,那么就不会执行destory()

但是会执行lastEffect.create(),打印出'classComponent:componentDidMount'

那么,App()第一次调用useEffect的源码解析流程就结束了,接下来看下多次调用useEffect的流程

八、updateEffect()

作用:

多次调用 useEffect 时,调用的函数

源码:
//多次更新时,走这里
function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  if (__DEV__) {
    //删除了 dev 代码
  }
  return updateEffectImpl(
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    create,
    deps,
  );
}
解析:

注意下updateEffectImpl()传的参数,跟二、mountEffectImpl()中传的参数一样

九、updateEffectImpl()

作用:

比较 deps 判断是否需要重新执行 useEffect 的 callback

源码:
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  // 当前正在 update 的 fiber 上的 hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;
  //currentHook:当前 fiber 对象上的 hook 对象
  //当currentHook不为空时
  if (currentHook !== null) {
    //获取旧 effect 状态
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    //如果 deps 参数存在的话
    if (nextDeps !== null) {
      //获取旧 deps 参数
      const prevDeps = prevEffect.deps;
      //比较前后 deps 是否相同
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        //如果相同的话,则表示没有 update,那么就传入 NoHookEffect tag
        pushEffect(NoHookEffect, create, destroy, nextDeps);
        //return 代表下面的代码都不执行了
        return;
      }
    }
  }
  //能执行到这里,说明currentHook=null 或者 deps 有 update
  //那么就添加 UpdateEffectTag
  sideEffectTag |= fiberEffectTag;
  //初始化 effect 链并返回
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
解析:

(1) 执行updateWorkInProgressHook(),获取当前正在 update 的 fiber 上的 hook

const hook = updateWorkInProgressHook();

(2) 获取 deps,方便与prevDeps比较,来决定是否更新

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

(3) 然后就是调用核心函数areHookInputsEqual(),比较前后 deps 是否相同

if (currentHook !== null) {
    //获取旧 effect 状态
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    //如果 deps 参数存在的话
    if (nextDeps !== null) {
      //获取旧 deps 参数
      const prevDeps = prevEffect.deps;
      //比较前后 deps 是否相同
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        //如果相同的话,则表示没有 update,那么就传入 NoHookEffect tag
        pushEffect(NoHookEffect, create, destroy, nextDeps);
        //return 代表下面的代码都不执行了
        return;
      }
    }
  }

如果areHookInputsEqual()返回的结果为 true 的话,说明该 effect 没有产生副作用,则为该 effect 添加NoHookEffect的 effectTag 表示不更新执行useEffect的 callback,并返回

(4) areHookInputsEqual()源码

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

因为 deps 是一个 Array,所以会循环去比较 array 中的每个 item

注意:
这里是用 Object.is() 去进行浅比较的,也就是说深比较是一定会更新的

(5) Object.is()源码

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

(6) 如果areHookInputsEqual()返回为false的话,就会执行下面的语句

  //能执行到这里,说明currentHook=null 或者 deps 有 update
  //那么就添加 UpdateEffectTag
  sideEffectTag |= fiberEffectTag;
  //初始化 effect 链并返回
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);

(7) 那么就会再次调用commitPassiveHookEffects()——>commitHookEffectList()

注意:
多次调用同一个 useEffect 的时候,会先去执行上一次的 destory(),再执行本次的create()

useEffect流程图

useEffect流程图.jpg
useEffect流程图.jpg

GitHub

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

flushPassiveEffects()
github.com/AttackXiaoJ…

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


(完)