实现一个精简React -- 实现useEffect(10)

有了实现useState的经验,我们还是从useEffect的使用方法上入手。

useEffect接收两个参数,一个是用于回调执行的的callback,一个是依赖项。

示例:

js 复制代码
const Foo = () => {
		const [count, setCount] = useState(1)
    React.useEffect(() => {
        console.log('init')
    }, [count])

    return (
        <div> Foo </div>
    )
}

代码实现

我们根据使用方法,就可以定义出useEffect函数,并传递两个参数。

js 复制代码
function useEffect(callback, deps) {
    let effectHook = {
        callback,
        deps,
    };
    // effectHook 挂载到wipFiber中
    wipFiber.effectHook = effectHook;
}

useEffect的执行时机是在dom挂载完成后执行的 ,所以可以在函数 commitRoot 中的 commitWork 后执行,也就是dom挂载完成后执行。

js 复制代码
function commitRoot() {
    deletions.forEach(commitDeletion);
    commitWork(wipRoot.child);
    // 定义一个函数commitEffectHook用来调用useEffect
    commitEffectHook();
    wipRoot = null;
    deletions = [];
}

commitEffectHook 方法用来执行useEffect,在使用react的useEffect时,正常情况下会有两种情况:

  1. 页面首次挂载后,会执行一次。(init)
  2. 依赖项发生改变的时候会执行。(update)

因此在commitEffectHook 的函数就可以根据两种情况来做

1、init

在commitEffectHook函数中,要获取到effectHooks中的callback,就需要去遍历整个dom树,因此需要一个递归函数。

js 复制代码
function commitEffectHook() {
    // run 函数用于递归
    function run(fiber) {
        if (!fiber) return;
        // 如果callback函数存在的话就调用。
	fiber.effectHook?.callback()
        run(fiber.child);
        run(fiber.sibling);
    }
    run(wipRoot);
}

**2、**update

更新时要针对useEffect的依赖项进行判断,如果依赖项发生了改变,才会调用,如果没有发生改变则不调用。

因此我们可以通过是否存在alternate来判断是不是首次挂载。

js 复制代码
// commitWork 后执行此方法
function commitEffectHook() {
    function run(fiber) {
        if (!fiber) return;
        // 如果fiber中没有alternate属性,则表示是首次挂载
        if (!fiber.alternate) { // init
            fiber.effectHook?.callback();
        } else { // update
            // 考虑到依赖项可能会有多个的情况,因此采用遍历的方法去挨个对比。
            // 对effectHook中的deps遍历对比oldDeps,如果不相等则调用callback
            fiber.effectHook?.deps.forEach((newDep, index) => {
                const oldDeps = fiber.alternate.effectHook?.deps;
                if (oldDeps[index] !== newDep) {
                    fiber.effectHook.callback();
                }
            });
            
            const oldEffectHook = fiber.alternate.effectHook
            const needUpdate = oldEffectHook?.deps.some((oldDep,index) => {
	            return oldDep !== fiber.effectHook.deps[index]
            })
            
            needUpdate && fiber.effectHook.callback()
        }
        // 根据链表递归调用
        run(fiber.child);
        run(fiber.sibling);
    }

    run(wipRoot);
}

但是我们考虑到useEffect的依赖项会有多个,因此按照上面的逻辑,后面的就会覆盖掉前面的,所以借鉴useState的实现,可以设置一个数组,将useEffect存储起来,然后循环调用即可。

js 复制代码
let effectHooks;  //设置一个数组
function useEffect(callback, deps) {
    let effectHook = {
        callback,
        deps,
    };
    effectHooks.push(effectHook);
    wipFiber.effectHooks = effectHooks;
}
js 复制代码
function commitEffectHook() {
    function run(fiber) {
        if (!fiber) return;

        if (!fiber.alternate) { // init
            if (fiber.effectHooks) {
                // 循环调用
                fiber.effectHooks.forEach((effect) => {
                    effect.callback();
                });
            }
        } else { // update
            // 双重循环,新值与旧值进行对比,不同则调用
            const oldEffectHooks = fiber.alternate.effectHooks;
            fiber.effectHooks?.forEach((newEffect, index) => {
                if (newEffect.deps.length) {
                    oldEffectHooks[index].deps.some((dep, i) => {
                        const needUpdate = dep !== newEffect.deps[i];
                        needUpdate && (newEffect.cleanup = newEffect.callback());
                    });
                }
            });
        }

        run(fiber.child);
        run(fiber.sibling);
    }

    run(wipRoot);
}

useEffect中的cleanup

cleanup在调用useEffect之前进行调用,当deps为空时不会调用cleanup

useEffect cleanup 是Hook中的一个函数,它允许我们在卸载组件之前整理代码

useEffect 挂钩可以返回一个函数。

cleanup 可防止内存泄漏并删除一些不必要和不需要的行为。

js 复制代码
React.useEffect(() => {
  console.log('count', count)
  return () => {
      console.log("cleanup")
  }
}, [count])

代码实现

useEffect 的 cleanup 是在组件卸载之前调用,也可以理解为,下次组件渲染时,如果有cleanup则先触发上一次的cleanup。

所以可以在出发useEffect的callback之前先触发cleanup

js 复制代码
let effectHooks;
function useEffect(callback, deps) {
    let effectHook = {
        callback,
        deps,
        cleanup: undefined,  // 设置并存储
    };
    effectHooks.push(effectHook);
    wipFiber.effectHooks = effectHooks;
}
js 复制代码
function commitEffectHook() {
    function run(fiber) {
        if (!fiber) return;

        if (!fiber.alternate) {
            // 初始化
            if (fiber.effectHooks) {
                fiber.effectHooks.forEach((effect) => {
                    // 初始化时将callback的返回值赋值给cleanup
                    effect.cleanup = effect.callback();
                });
            }
        } else {
            // update
            const oldEffectHooks = fiber.alternate.effectHooks;
            fiber.effectHooks?.forEach((newEffect, index) => {
                if (newEffect.deps.length) {
                    oldEffectHooks[index].deps.some((dep, i) => {
                        const needUpdate = dep !== newEffect.deps[i];
												// 同理将callback的返回值赋值给cleanup
                        needUpdate &&
                            (newEffect.cleanup = newEffect.callback());
                    });
                }
            });
        }

        run(fiber.child);
        run(fiber.sibling);
    }

    // cleanup的调用时机是第二次调用时先执行上一次的cleanup
    // 所以也要从根节点开始遍历去调用,但是!!此次调用的是上一次的,所以遍历的是alternate
    function runCleanup(fiber) {
        if (!fiber) return;
        fiber.alternate?.effectHooks?.forEach((hook) => {
            if (hook.deps.length) {
                hook.cleanup && hook.cleanup();
            }
        });

        runCleanup(fiber.child);
        runCleanup(fiber.sibling);
    }

    runCleanup(wipRoot);
    run(wipRoot);
}
相关推荐
孟陬21 小时前
React Router Declarative → Data → Framework 三种模式如何选
react.js
小妖怪的夏天21 小时前
react native 出现 FATAL EXCEPTION: OkHttp Dispatcher
react native·react.js·okhttp
晓得迷路了21 小时前
栗子前端技术周刊第 97 期 - Viteland:8 月回顾、Redux Toolkit 2.9、Nuxt 4.1...
前端·javascript·nuxt.js
前端双越老师21 小时前
前端开发 AI Agent 智能体,需要掌握哪些知识?
前端·node.js·agent
EndingCoder21 小时前
Electron 安全性最佳实践:防范常见漏洞
前端·javascript·electron·前端框架·node.js·桌面端
学前端搞口饭吃21 小时前
React props的使用
前端·javascript·react.js
灵感__idea21 小时前
JavaScript高级程序设计(第5版):前端的能力边界
前端·javascript·程序员
华洛21 小时前
SEO还没死,GEO之战已经开始
前端·javascript·产品
IT_陈寒21 小时前
Python性能优化:5个被低估的魔法方法让你的代码提速50%
前端·人工智能·后端
As33100101 天前
Chrome 插件开发入门指南:从基础到实践
前端·chrome