前言
想必各位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 = () => {// useEffect1useEffect(() => {}, []);useLayoutEffect(() => {}, []);// useEffect2useEffect(() => {}, []);return (<div>demo</div>)};// 例子代码 const Demo = () => { // useEffect1 useEffect(() => {}, []); useLayoutEffect(() => {}, []); // useEffect2 useEffect(() => {}, []); return (<div>demo</div>) };// 例子代码 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添加flagscurrentlyRenderingFiber.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永远指向最后一个effectconst 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;}// 可先忽略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; }// 可先忽略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收集的effectsif ((finishedWork.subtreeFlags & Passive) !== 0|| (finishedWork.flags & Passive) !== 0) {// 宏任务setTimeout(() => {// 这里表示的是一个函数,这个函数会将所有的effects进行一个过滤,这里会过滤出useEffect接收的effects,也就是含有HookPassive标识的effectseffects(HookPassive);}, 0);}// 微任务queueMicrotask(() => {// 这里表示的是一个函数,这个函数会将所有的effects进行一个过滤,这里会过滤出useLayoutEffect接收的effects,也就是含有HookLayout标识的effectseffects(HookLayout);});}// 提交副作用 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); }); }// 提交副作用 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的destorycommitMountEffects(flag);};const effects = (flag) => { // 执行effect身上的destory函数,这个destory函数是只有执行过effect本身才会有值,所以mountEffect的时候是没有的,仅会在后续的updateEffect执行。 commitUnmountEffects(flag); // 执行effect并更新/新增effect的destory commitMountEffects(flag); };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;};};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; }; };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的代码:点这里
完整版源码:点这里