实现一个精简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);
}
相关推荐
咖啡の猫43 分钟前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲3 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5814 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路4 小时前
GeoTools 读取影像元数据
前端
ssshooter4 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry5 小时前
Jetpack Compose 中的状态
前端
dae bal6 小时前
关于RSA和AES加密
前端·vue.js
柳杉6 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog6 小时前
低端设备加载webp ANR
前端·算法
LKAI.6 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi