实现一个精简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);
}
相关推荐
Aliex_git8 小时前
跨域请求笔记
前端·网络·笔记·学习
37方寸8 小时前
前端基础知识(Node.js)
前端·node.js
早點睡3908 小时前
基础入门 React Native 鸿蒙跨平台开发:react-native-flash-message 消息提示三方库适配
react native·react.js·harmonyos
powerfulhell9 小时前
寒假python作业5
java·前端·python
木子啊9 小时前
前端组件化:模板继承拯救发际线
前端
三十_A9 小时前
零基础通过 Vue 3 实现前端视频录制 —— 从原理到实战
前端·vue.js·音视频
前端小菜袅9 小时前
PC端原样显示移动端页面方案
开发语言·前端·javascript·postcss·px-to-viewport·移动端适配pc端
We་ct9 小时前
LeetCode 228. 汇总区间:解题思路+代码详解
前端·算法·leetcode·typescript
爱问问题的小李9 小时前
ue 动态 Key 导致组件无限重置与 API 重复提交
前端·javascript·vue.js
早點睡3909 小时前
高级进阶 ReactNative for Harmony项目鸿蒙化三方库集成实战:react-native-image-picker(打开手机相册)
react native·react.js·harmonyos