React源码系列(七)------ useEffect
前言
想必各位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>)
};
可以看到,只要是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中。
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的流程图,这张流程图更多的是为了给调试结尾处的仓库的代码所使用。
结尾
本文通过Demo组件例子讲述effect hooks的执行顺序,再通过Demo组件和部分简化过的源码讲述effect hooks都做了些什么并且引出真正的主角updateQueue,最后讲述updateQueue的执行时机以及如何执行。
读完本文,仅是理解effect hooks如何执行,以及原理是完全没问题的,但想要足够深的理解这些effect hooks,更推荐各位调试以下仓库的代码。
仅包含effect hooks的代码:点这里
完整版源码:点这里
转载自:https://juejin.cn/post/7245292130624946236