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. 存在副作用
0b001
2. 有 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上
其结果如下
重点:
- hook的memoizedState跟useState不同,不是一个 intitalValue或者一个 func,而是一个effect的对象。
- effect一方面保存到hook链上;也保存到 updateQueue副作用链上;
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,
);
}
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 的执行
在函数组件useEffect执行的时候,其只是构建effect对象并将其保存在 hook链 和 updateQueue副作用链上,其具体怎么执行,怎么判断是否需要重新执行;什么执行 destroy? 等都是在组件渲染的流程中进行的。具体可以看Scheduler任务调度的commit阶段的 commitBeforeMutationEffects
对于渲染阶段其主要分为三个步骤
- commitBeforeMutationEffects
- commitMutationEffects
- commitLayoutEffects
commitBeforeMutaition
flushPassiveEffects
js
/**
* 执行当前副作用中的 useEffect 副作用
* 1. 执行useEffect的销毁函数 useEffect(() => () => { // 销毁函数 })
* 2. 执行useEffect的创建函数
* @returns
*/
function flushPassiveEffectsImpl() {
// 第一步: 处理销毁函数
const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {
// .... 执行 effect.desctroy
}
// Second pass: Create new passive effects.
// 第二步: 执行useEffect 的 创建函数
const mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (let i = 0; i < mountEffects.length; i += 2) {
// .... 执行 effect.create
}
return true;
}
这里面涉及到两个参数
- pendingPassiveHookEffectsUnmount 保存了当前需要卸载的effect的列表
- pendingPassiveHookEffectsMount 保存了当前需要create的effect的列表
commitMutationEffects
在commitMutation 会处理组件的 commitLifeCycles生命周期,如遇到函数组件会执行一个方法 ,
从而通过遍历updateQueue.lastEffect上的tag == 5 的副作用,加入到pendingPassiveHookEffectsUnmount 、pendingPassiveHookEffectsMount ,在下一个任务队列通过flushPassiveEffects 去 先执行 destroy 再执行 create
js
/**
* 函数组件 在 commitMutationEffects 阶段执行
* 收集当前 FiberNode上的 useEffect 类型的副作用,并将其加入到
* - pendingPassiveHookEffectsUnmount destroy类型的
* - pendingPassiveHookEffectsMount create 类型的
* @param {*} finishedWork
*/
function schedulePassiveEffects(finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const {next, tag} = effect;
// 判断是 useEffect 类型的 effect
if (
(tag & HookPassive) !== NoHookEffect &&
(tag & HookHasEffect) !== NoHookEffect
) {
//
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
enqueuePendingPassiveHookEffectMount(finishedWork, effect);
}
effect = next;
} while (effect !== firstEffect);
}
}
useLayoutEffect
作用
其函数签名与 useEffect
相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect
以避免阻塞视觉更新。
useLayoutEffect的保存
js
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
);
}
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
}
可见在生成阶段其就是 FiberNode.flags 和 effect.tag不同
useLayoutEffect 和 useEffect 的异同点
相同点
都是effect类型的hook
- 都会在函数组件执行阶段在FiberNode.memoizedState 上生成一个 hook 对象
- 都会在函数组件执行阶段在FiberNode.updateQueue 上添加一个 effect副作用
缓存数据结构、语法(deps、create、destroy)都很像
不同点
执行时机不同
useLayoutEffect
- create在commit的最后阶段(commitLayoutEffects)中才会执行,并生成destroy
- destroy在commit的第二阶段(commitMutationEffects)中如果是函数类型组件的卸载操作,就会执行destroy 回调
useEffect是在
flushPassiveEffects 的时候统一执行(destroy -> create)
执行的方式不同
- useLayoutEffect 他是在组件Commit阶段同步执行的
- useLayout : 在组件Commit阶段收集,然后通过scheduler高优先级任务队列回调执行的