什么是 useEffect
useEffect 是执行副作用的 hook,执行时机是在页面绘制完成后,在代码中即提交阶段结束后。它接收两个参数,effect 回掉函数和 deps 依赖项。
-
effect回调:在渲染完成、画面更新到 DOM 之后执行。- 可以在这里做数据请求、订阅事件、手动操作 DOM、注册定时器等。
- 如果
effect返回一个函数,则该返回函数被视为“清理函数”(cleanup),会在组件卸载或下一次执行新的 effect 之前调用,用来释放资源/取消订阅/清除定时器等。
-
deps依赖数组(可选):- 不传:每次渲染后都执行 effect。
- 空数组
[]:仅在首次挂载后执行一次,相当于componentDidMount。 [a, b, c]:只有当其中某个依赖值发生变化时,才会重新执行 effect,相当于“有条件地”模拟componentDidUpdate。
import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setData);
}, [userId]); // 只有 userId 变化时才重新请求
}
实现
数据结构图:
在代码中,维护了一个 effect 循环链表的数据结构,每个 effect 和 useEffect 一一对应,并且在当前渲染 fiber 上新增一个 updateQueue 字段,这个字段中的 lastEffect 字段指向 effect 循环链表中的最后一个 effect。
代码实现
首先需要新增一个 hook。
// 省略导出新增,删除其他 hook 相关代码
const HooksDispatcherOnMount = {
useEffect: mountEffect,
};
const HooksDispatcherOnUpdate = {
useEffect: updateEffect,
};
对于 hook 开发,会有一个套路,要新增一个 hook 需要配套 mount 阶段执行函数和 update 阶段执行函数。
import { Passive as PassiveEffect } from "./ReactFiberFlags";
import { HasEffect as HookHasEffect, Passive as HookPassive } from "./ReactHookEffectTags";
function mountEffect(create, deps) {
// 省略日志等代码
return mountEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, undefined, nextDeps);
}
/**
* 添加 effect 链表
* @param {*} tag effect 的标签
* @param {*} create 创建方法
* @param {*} destroy 销毁方法
* @param {*} deps 依赖数组
*/
function pushEffect(tag, create, destroy, deps) {
const effect = {
tag,
create,
destroy,
deps,
next: null,
};
let componentUpdateQueue = currentRenderingFiber.updateQueue;
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentRenderingFiber.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
}
return effect;
}
在 mount 阶段,完成了对上图中数据结构的构建。接下来需要消费这个数据结构。
useEddect hook 的执行时机是在页面绘制完成后,那么页面绘制是在提交阶段,消费 effect 数据结构的地方也是在提交阶段。
const { finishedWork } = root;
if ((finishedWork.subtreeFlags & Passive) !== NoFlags || (finishedWork.flags & Passive) !== NoFlags) {
if (!rootDoesHavePassiveEffect) {
rootDoesHavePassiveEffect = true;
scheduleCallback(flashPassiveEffect);
}
}
在提交阶段,根据根 fiber 的 flags 和 subtreeFlags 字段,判断当前 fiber 和 子 fiber 是否存在副作用,如果存在则执行添加一个计划,执行 flashPassiveEffect 函数,并将 rootDoesHavePassiveEffect 变量置为 true,这个变量表示当前根 fiber 存在副作用需要执行,在 DOM 变更执行完成后,重新初始化。
function flashPassiveEffect() {
if (rootWithPendingPassiveEffects !== null) {
const root = rootWithPendingPassiveEffects;
// 执行卸载副作用
commitPassiveUnmountEffects(root.current);
// 执行挂载副作用
commitPassiveMountEffects(root, root.current);
}
}
export function commitPassiveUnmountEffects(finishedWork) {
commitPassiveUnmountOnFiber(finishedWork);
}
function commitPassiveUnmountOnFiber(finishedWork) {
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case HostRoot: {
recursivelyTraversePassiveUnmountEffects(finishedWork);
break;
}
case FunctionComponent: {
recursivelyTraversePassiveUnmountEffects(finishedWork);
if (flags & Passive) {
commitHookPassiveUnmountEffects(finishedWork, HookPassive | HookHasEffect);
}
}
}
}
function recursivelyTraversePassiveUnmountEffects(parentFiber) {
if (parentFiber.subtreeFlags & Passive) {
let child = parentFiber.child;
while (child !== null) {
commitPassiveUnmountOnFiber(child);
child = child.sibling;
}
}
}
function commitHookPassiveUnmountEffects(finishedWork, hookFlags) {
commitHookEffectListUnmount(hookFlags, finishedWork);
}
function commitHookEffectListUnmount(flags, finishedWork) {
const updateQueue = finishedWork.updateQueue;
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
const destroy = effect.destroy;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
export function commitPassiveMountEffects(root, finishedWork) {
commitPassiveMountOnFiber(root, finishedWork);
}
function commitPassiveMountOnFiber(finishedRoot, finishedWork) {
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case HostRoot: {
recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork);
break;
}
case FunctionComponent: {
recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork);
if (flags & Passive) {
commitHookPassiveMountEffects(finishedWork, HookPassive | HookHasEffect);
}
}
}
}
function recursivelyTraversePassiveMountEffects(root, parentFiber) {
if (parentFiber.subtreeFlags & Passive) {
let child = parentFiber.child;
while (child !== null) {
commitPassiveMountOnFiber(root, child);
child = child.sibling;
}
}
}
function commitHookPassiveMountEffects(finishedWork, hookFlags) {
commitHookEffectListMount(hookFlags, finishedWork);
}
function commitHookEffectListMount(flags, finishedWork) {
const updateQueue = finishedWork.updateQueue;
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
这里的逻辑就是递归所有节点,并判断是否存在副作用,如果存在则执行,和提交阶段对 DOM 的处理非常相似。
function updateEffect(create, deps) {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps != null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
currentRenderingFiber.flags = fiberFlags;
hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, destroy, nextDeps);
}
在 update 阶段,逻辑其实比较简单,就是根据 deps 是否变化,来判断是否设置 flags,设置了 flags 则会在 commit 阶段进入执行副作用的逻辑。但是对于 effect 对象来说,不管 deps 是不是有变化,都需要更新。