Appearance
useEffect
useEffect 是什么?
该 Hook 接收一个 包含命令式、且可能有副作用代码 的函数。
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。
栗子
deps 为 [], 模拟的是类组件的 constructor 的功能,函数组件初次渲染的时候执行,更新的时候不执行
js
// 1. deps 为 [], 模拟的是类组件的 constructor 的功能
React.useEffect(() => {
console.log("这是一个只有函数初始化才会执行的 effect");
}, []);deps为 [num]; 模拟的是 Vue 的 watch 的功能(immediate:true,初次也会执行),在 num 改变的时候会重新执行
js
// 2. deps为 [num]; 模拟的是Vue的watch的功能(immediate:true,初次也会执行)
React.useEffect(() => {
console.log("effect可以添加deps,在deps改变的时候也会再次执行");
}, [num]);函数返回值为 function;用于进行副作用的清除工作;执行时机为
- 在 effect 的组件要被卸载之前;
- deps 更新重新执行 create 之前;
js
// 2. `函数返回值为 function;` 用于进行副作用的清除工作;
// 执行时机为 1. 在effect的组件要被卸载之前; 2. deps更新重新执行create之前
React.useEffect(() => {
const timer = setTimeout(() => {
setNum(2);
}, 1000);
// useEffect 可以返回一个函数(destroy)
// 此函数在组件 1. 组件卸载前、2. 在执行下一个 effect 之前,上一个 effect 就已被清除 的时候会执行
return () => {
clearTimeout(timer);
};
}, []);
flags
Hooks 类型的副作用其 flag 文件保存在 ReactHookEffectTags.js 文件中,其内容如下:
js
export type HookFlags = number;
export const NoFlags = /* */ 0b000;
// Represents whether effect should fire.
// Hook 是否存在副作用
export const HasEffect = /* */ 0b001;
// Represents the phase in which the effect (not the clean-up) fires.
// useLayoutEffect 类型的副作用
export const Layout = /* */ 0b010;
// useEffect 类型的副作用
export const Passive = /* */ 0b100;那么对于 useLayoutEffect 和 useEffect 这两个 Hook 其 tag 分别是什么?
- useEffect
tag : 0b101转换为 10 进制为 5
原因: 因为其 1. 存在副作用0b001 2. 有 Passive 类型的副作用0b100 ; 所以合并起来就是 0b101
结果: 就存在这样的判断 (tag & HookPassive) !== NoHookEffect && (tag & HookHasEffect) !== NoHookEffect ) 去是否是 useEffect 类型的副作用
useLayoutEffect
tag :
0b011转换为 10 进制为3原因: 因为其 1. 存在副作用
0b0012. 有 Layout 类型的副作用0b100; 所以合并起来就是0b011结果: 就存在这样的判断
(tag & HookLayout) !== NoHookEffect && (tag & HookHasEffect) !== NoHookEffect )去是否是 useLayoutEffect 类型的副作用
useEffect 的 保存
跟 useState 一样,useEffect 的执行也分为两种,一种在组件初次渲染的时候;一种在组件更新渲染的时候;
其核心流程都很相似,主要分为下面步骤:
创建或者获取 hook
在 FiberNode 上标记当前组件存在副作用。
FiberNode.flags |= fiberFlags ;(HookPassive)hook.memoizedState = pushEffect()- 创建 effect 对象
- 在 FiberNode.updateQueue.lastEffect 创建或者在首位插入当前 effect
- 将 effect 作为
memoizedState保存在 effect 的 hook 上
其结果如下

重点:
- effect 类型的副作用 hook 的 memoizedState 跟 useState 渲染的数据类型不同,其不是一个 intitalValue 或者一个 func,而是一个effect 的对象。
- effect 一方面保存到 hook 链上;也保存到 updateQueue 副作用链上;
- hook 链上用来在更新阶段通过
updateWorkInProgressHook()去获取对应的 hook 实例对象 - FiberNode.updateQueue 副作用链上 是用来表明这是一个 Update 副作用,需要在处理任务的时候进行副作用的处理
具体如下:
mountEffect
初次渲染的流程。可见就是构建 useEffect 的 hook 链 和 updateQueue 的副作用链
js
/**
* useEffect 和 useLayoutEffect 初次渲染的实现方法
* @param {*} fiberFlags
* @param {*} hookFlags
* @param {*} create
* @param {*} deps
*/
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 构建 hook
const hook = mountWorkInProgressHook();
// 创建 deps 订阅依赖
const nextDeps = deps === undefined ? null : deps;
// 在FiberNode上标记存在 HookPassive 的 effect
currentlyRenderingFiber.flags |= fiberFlags;
// 创建 effect , 并将 effect 添加到 1. FiberNode.updateQueue 2. hook.memoizedState
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps
);
}其中有两个重点入参:fiberFlags、hookFlags 分别是什么作用?
fiberFlags
标记 Hook 存在 Update 类型的副作用,会标记在组件 FiberNode.flags 上 并在组件 commit 的去处理副作用从而生成新的 Update 任务
hookFlags
标志 Hook 副作用的执行时机, 如 Layout、Passive、Inserion 等
updateEffect
跟 mountEffect 的区别就是在 hook 作用链上 如果可以复用原来的 hook 就直接复用不需要重新生成
js
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 找到对应的 hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
// 在 updateWorkInProgressHook 会通过全局变量currentHook去指向当前的 hook
if (currentHook !== null) {
// 获取旧的effect对象
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
// 如果依赖存在
if (nextDeps !== null) {
// 旧的依赖
const prevDeps = prevEffect.deps;
// 判断依赖是否相等
// 1. deps 一般为数组 需要下标和内容完全相等 [1,2,3] !== [1,3,2]
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 再次构建 FiberNode.updateQueue
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 在FiberNode上标记存在 HookPassive 的 effect
currentlyRenderingFiber.flags |= fiberFlags;
// 创建 effect , 并将 effect 添加到 1. FiberNode.updateQueue 2. hook.memoizedState
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps
);
}pushEffect
生成 effect 的对象,并将 effect 添加到 FiberNode.updateQueue 中
js
/**
* 生成effect的对象,并将effect添加到 FiberNode.updateQueue中
* @param {*} tag
* @param {*} create
* @param {*} destroy
* @param {*} deps
* @returns
*/
function pushEffect(tag, create, destroy, deps) {
// 构建effect的对象
const effect: Effect = {
// effect的类型。就两种
// useEffect -- 5
// useLayoutEffect -- 5
tag,
// effect的执行函数
create,
// effect执行函数返回值 , 作为effect的销毁函数
destroy,
// 依赖
deps,
// Circular
next: (null: any),
};
// 获取FiberNode.updateQueue
let componentUpdateQueue: null | FunctionComponentUpdateQueue =
(currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
// updateQueue 那就初始化 并添加到 updateQueue.lastEffect 上
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// updateQueue不为空 , 生成一个以当前effect为第一位的 环状单项链表结构
// updateQueue.lastEffect === currentEffect
// currentEffect.next = oldlastEffect
// oldlastEffect.next = currentEffect
const lastEffect = 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;
}
执行时机
在函数组件 useEffect 执行的时候,其只是构建 effect 对象并将其保存在 hook 链 和 updateQueue 副作用链上,其具体怎么执行?怎么判断是否需要重新执行;什么执行 destroy?
这些都是在组件渲染的流程中进行的。具体可以看 Scheduler 任务调度的 commit 阶段的 flushPassiveEffects
flushPassiveEffects
js
/**
* 执行当前副作用中的 useEffect 副作用
* 1. 执行useEffect的销毁函数 useEffect(() => () => { // 销毁函数 })
* 2. 执行useEffect的创建函数
* @returns
*/
function flushPassiveEffectsImpl() {
// 执行 需要被删除的旧节点的 , 存在Passive副作用的节点的 destroy 函数
commitPassiveUnmountEffects(root.current);
// 执行 需要被添加的新节点的, 存在Passive副作用的节点的 create 函数
commitPassiveMountEffects(root, root.current, lanes, transitions);
return true;
}这里面涉及到两个参数
- commitPassiveUnmountEffects 执行 需要被删除的旧节点的 , 存在 Passive 副作用的节点的 destroy 函数
- commitPassiveMountEffects 执行 需要被添加的新节点的, 存在 Passive 副作用的节点的 create 函数
具体可以看 flushPassiveEffects
其通过两次深度优先遍历 去处理整个 FiberNode 中所有存在副作用的节点有关 useEffect 类型副作用。
其中在
commitPassiveUnmountEffects阶段对于存在DeletionChild标记的节点,按照 先父后子 的顺序依次执行 函数式组件中存在的 useEffect 副作用的destroy函数在
commitPassiveMountEffects阶段,再进行一次深度优先的遍历,去处理所有存在PassiveMask标记的节点的 函数式组件中存在的 useEffect 副作用的create函数
结论
执行时机
useEffect的destroy函数: 在flushPassiveEffects阶段执行,所以其在 DOM 更新之前useEffect的create函数: 在flushPassiveEffects阶段执行,所以其在 DOM 更新之前
执行顺序
useEffect的destroy函数: 在flushPassiveEffects阶段执行,按照先父后子的顺序执行useEffect的create函数: 在flushPassiveEffects阶段执行,按照先子后父的顺序执行
关键信息
- fiberFlags
- 初始化渲染阶段 : PassiveEffect | PassiveStaticEffect
- 更新 渲染阶段 : PassiveEffect
- hookFlags
- 初始化渲染阶段 : HookPassive
- 更新 渲染阶段 : HookPassive
