likes
comments
collection
share

React源码系列(七)------ useEffect

作者站长头像
站长
· 阅读数 19

前言

想必各位React使用者,平时开发中除了useState外,使用最多的莫过于useEffect了,因为我们经常需要使用useEffect处理各种副作用,绑定/解绑时间、添加/消除定时器、发起请求等。本文就带着各位一步一步了解整个useEffect执行过程。

effects

到目前为止effect hooks其实已经有3种了,分别是useInsertionEffect、useLayoutEffect以及useEffect。

useInsertionEffect主要面向库(css in js)开发者使用的,不是我们今天的主角。这里重点将useLayoutEffect和useEffect。

我们都知道useLayoutEffect的执行是在useEffect之前的,而且effect hooks的执行都会在dom挂载完成之后,这样能保证在effect hooks中拿到真实dom,进行各种副作用操作。那么React又是如何保证以上这些的呢?其实主要得益于事件循环机制,我们来看下图。

// 例子代码
const Demo = () => {
  // useEffect1
  useEffect(() => {}, []);
  
  useLayoutEffect(() => {}, []);

  // useEffect2
  useEffect(() => {}, []);
  
  return (<div>demo</div>)
};

React源码系列(七)------ useEffect

可以看到,只要是useEffect都会扔到宏任务中,useLayoutEffect会扔到微任务中,而React执行commitRoot的那些诸如appendChild等操作是在主队列中的,因此确保了上述说的那几个点。

effect hook执行

其实effect hooks的执行是非常简单的,短短几十行代码就能了解清楚。

// 可先忽略HookPassive这两个参数,对于在此处对effect hooks的执行无太多作用
// PassiveEffect是一个标识,表示之后要执行这个effect函数
function mountEffect(create, deps) {
    return mountEffectImpl(Passive, HookPassive, create, deps);
}

// 可先忽略HookLayout这两个参数,对于在此处对effect hooks的执行无太多作用
// UpdateEffect是一个标识,表示之后要执行这个effect函数
function mountLayoutEffect(create, deps) {
    return mountEffectImpl(Update, HookLayout, create, deps);
}

function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
    // mountWorkInProgressHook作用是初始化一个hook,生成一个有基本hook属性的对象
    const hook = mountWorkInProgressHook();
    // effect hooks的依赖数组
    const nextDeps = deps === undefined ? null : deps;
    // 可先忽略此步  给当前的函数组件fiber添加flags
    currentlyRenderingFiber.flags |= fiberFlags;
    hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, undefined, nextDeps);
}

/**
 * 添加effect链表
 * @date 2023-04-01
 * @param {any} tag effect的标签
 * @param {any} create 创建方法
 * @param {any} destroy 销毁方法
 * @param {any} deps 依赖数组
 * @returns effect
 */
function pushEffect(tag, create, destroy, deps) {
    const effect = {
        tag,
        create,
        destroy,
        deps,
        next: null,
    };
    // fiber的更新队列
    let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
    if (componentUpdateQueue === null) {
        // createFunctionComponentUpdateQueue是构建fiber身上的hook链表
        componentUpdateQueue = createFunctionComponentUpdateQueue();
        currentlyRenderingFiber.updateQueue = componentUpdateQueue;
        componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
        // lastEffect永远指向最后一个effect
        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;
}

上面这段代码基本上就是effects hook的执行过程了,虽然他仅是mountHook。而在updateHook中,仅是对比下依赖数组是否一样,不一样就更新一下就将标识改一下,表示这个effect要执行。

useEffect就是这么简单,他没有大家想的那么神秘,看着功能如此强大的effect hooks,模拟了类式组件的各种生命周期函数,而他所做的事情竟只有这么点,仅仅是将接受到的函数(effect)扔进fiber的updateQueue中。

React源码系列(七)------ useEffect

updateQueue

useEffect能够模拟出类式组件的那么多种生命周期函数,其实精髓就在这个updateQueue中,所以我们其实真正要弄明白的是updateQueue是做什么的,而其中存的effects链表又什么时候会执行,上面代码中Passive、HookPassive、Update、HookLayout又发挥着怎样的作用。

Passive、HookPassive、Update、HookLayout

  • Passive、Update都是fiber的flag之一,它们都是一个普通的二进制数,它和之前那些文章中提到的更新、插入删除标识是一种东西,都是fiber的tag,标示要执行某种操作。

  • HookPassive、HookLayout是effect上的tag,他表示这个effect是useEffect接收的函数还是useLayoutEffect接收的函数。

updateQueue的执行

updateQueue被使用时,是在commitRoot阶段,我们来看commitRoot中一小段代码。

// 提交副作用
function commitRoot(root) {
    // finishedWork就是一个fiber
    // finishedWork.subtreeFlags & Passive,这个表达式就是为了校验fiber身上有没需要执行的useEffect收集的effects
    if ((finishedWork.subtreeFlags & Passive) !== 0
        || (finishedWork.flags & Passive) !== 0) {
        // 宏任务
        setTimeout(() => {
          // 这里表示的是一个函数,这个函数会将所有的effects进行一个过滤,这里会过滤出useEffect接收的effects,也就是含有HookPassive标识的effects
          effects(HookPassive);
        }, 0);
    }
    // 微任务
    queueMicrotask(() => {
      // 这里表示的是一个函数,这个函数会将所有的effects进行一个过滤,这里会过滤出useLayoutEffect接收的effects,也就是含有HookLayout标识的effects
      effects(HookLayout);
    });
}

通过上面这一小段commitRoot代码可以看到正如之前所说,useEffect被扔进宏任务中,useLayoutEffect被扔进微任务中,当开始执行这些微任务或者宏任务时,commitRoot已经执行完了,提交dom的操作结束了。

接下来我们再看看上面代码中所说的effects。

const effects = (flag) => {
    // 执行effect身上的destory函数,这个destory函数是只有执行过effect本身才会有值,所以mountEffect的时候是没有的,仅会在后续的updateEffect执行。
    commitUnmountEffects(flag);
    // 执行effect并更新/新增effect的destory
    commitMountEffects(flag);
};

上面这段代码执行effects也是比较容易的。

const commitUnmountEffects = (flag) => {
  const lastEffect = fiber.updateQueue.lastEffect;
  let curEffect = lastEffect.next;
  while (curEffect) {
    if (effect.tag & flag !== 0) {
      // effect hooks接收到的那个函数的返回值
      if (effect.destory) {
        effect.destorye();
      }
    }
    curEffect = curEffect.next;
  };
};
const commitMountEffects = (flag) => {
  const lastEffect = fiber.updateQueue.lastEffect;
  let curEffect = lastEffect.next;
  while (curEffect) {
    if (effect.tag & flag !== 0) {
      // effect hooks接收到的那个函数
      effect.destory = effect.create();
    }
    curEffect = curEffect.next;
  };
};

到此整个effect hooks的执行过程已经全部讲完了,下面是一张useEffect的流程图,这张流程图更多的是为了给调试结尾处的仓库的代码所使用。

React源码系列(七)------ useEffect

结尾

本文通过Demo组件例子讲述effect hooks的执行顺序,再通过Demo组件和部分简化过的源码讲述effect hooks都做了些什么并且引出真正的主角updateQueue,最后讲述updateQueue的执行时机以及如何执行。

读完本文,仅是理解effect hooks如何执行,以及原理是完全没问题的,但想要足够深的理解这些effect hooks,更推荐各位调试以下仓库的代码。

仅包含effect hooks的代码:点这里

完整版源码:点这里

上一篇:DOM-DIFF