有了实现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时,正常情况下会有两种情况:
- 页面首次挂载后,会执行一次。(init)
- 依赖项发生改变的时候会执行。(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);
}