实现一个精简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);
}
相关推荐
A_aspectJ2 小时前
【Bootstrap V4系列】学习入门教程之 组件-输入组(Input group)
前端·css·学习·bootstrap·html
兆。3 小时前
电子商城后台管理平台-Flask Vue项目开发
前端·vue.js·后端·python·flask
互联网搬砖老肖3 小时前
Web 架构之负载均衡全解析
前端·架构·负载均衡
sunbyte3 小时前
Tailwind CSS v4 主题化实践入门(自定义 Theme + 主题模式切换)✨
前端·javascript·css·tailwindcss
湛海不过深蓝4 小时前
【css】css统一设置变量
前端·css
程序员的世界你不懂5 小时前
tomcat6性能优化
前端·性能优化·firefox
爱吃巧克力的程序媛5 小时前
QML ProgressBar控件详解
前端
进取星辰5 小时前
21、魔法传送阵——React 19 文件上传优化
前端·react.js·前端框架
wqqqianqian5 小时前
国产linux系统(银河麒麟,统信uos)使用 PageOffice 在线打开Word文件,并用前端对话框实现填空填表
linux·前端·word·pageoffice
BillKu5 小时前
CSS实现图片垂直居中方法
前端·javascript·css