title |
---|
Hook 原理(副作用Hook) |
本节建立在前文Hook 原理(概览)和Hook 原理(状态 Hook)的基础之上, 重点讨论useEffect, useLayoutEffect
等标准的副作用Hook
.
在fiber
初次构造阶段, useEffect
对应源码mountEffect, useLayoutEffect
对应源码mountLayoutEffect
mountEffect
:
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect | PassiveEffect, // fiberFlags
HookPassive, // hookFlags
create,
deps,
);
}
mountLayoutEffect
:
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect, // fiberFlags
HookLayout, // hookFlags
create,
deps,
);
}
可见mountEffect
和mountLayoutEffect
内部都直接调用mountEffectImpl, 只是参数不同.
mountEffectImpl
:
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 1. 创建hook
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 2. 设置workInProgress的副作用标记
currentlyRenderingFiber.flags |= fiberFlags; // fiberFlags 被标记到workInProgress
// 2. 创建Effect, 挂载到hook.memoizedState上
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags, // hookFlags用于创建effect
create,
undefined,
nextDeps,
);
}
mountEffectImpl
逻辑:
- 创建
hook
- 设置
workInProgress
的副作用标记:flags |= fiberFlags
- 创建
effect
(在pushEffect
中), 挂载到hook.memoizedState
上, 即hook.memoizedState = effect
- 注意:
状态Hook
中hook.memoizedState = state
- 注意:
function pushEffect(tag, create, destroy, deps) {
// 1. 创建effect对象
const effect: Effect = {
tag,
create,
destroy,
deps,
next: (null: any),
};
// 2. 把effect对象添加到环形链表末尾
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
// 新建 workInProgress.updateQueue 用于挂载effect对象
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
// updateQueue.lastEffect是一个环形链表
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
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;
}
}
// 3. 返回effect
return effect;
}
pushEffect
逻辑:
- 创建
effect
. - 把
effect
对象添加到环形链表末尾. - 返回
effect
.
effect
的数据结构:
export type Effect = {|
tag: HookFlags,
create: () => (() => void) | void,
destroy: (() => void) | void,
deps: Array<mixed> | null,
next: Effect,
|};
-
effect.tag
: 二进制属性, 代表effect
的类型(源码).export const NoFlags = /* */ 0b000; export const HasEffect = /* */ 0b001; // 有副作用, 可以被触发 export const Layout = /* */ 0b010; // Layout, dom突变后同步触发 export const Passive = /* */ 0b100; // Passive, dom突变前异步触发
-
effect.create
: 实际上就是通过useEffect()
所传入的函数. -
effect.deps
: 依赖项, 如果依赖项变动, 会创建新的effect
.
renderWithHooks
执行完成后, 我们可以画出fiber
,hook
,effect
三者的引用关系:
现在workInProgress.flags
被打上了标记, 最后会在fiber树渲染
阶段的commitRoot
函数中处理. (这期间的所有过程可以回顾前文fiber树构造/fiber树渲染
系列, 此处不再赘述)
站在fiber,hook,effect
的视角, 无需关心这个hook
是通过useEffect
还是useLayoutEffect
创建的. 只需要关心内部fiber.flags
,effect.tag
的状态.
所以useEffect
与useLayoutEffect
的区别如下:
fiber.flags
不同
- 使用
useEffect
时:fiber.flags = UpdateEffect | PassiveEffect
. - 使用
useLayoutEffect
时:fiber.flags = UpdateEffect
.
effect.tag
不同
- 使用
useEffect
时:effect.tag = HookHasEffect | HookPassive
. - 使用
useLayoutEffect
时:effect.tag = HookHasEffect | HookLayout
.
完成fiber树构造
后, 逻辑会进入渲染
阶段. 通过fiber 树渲染中的介绍, 在commitRootImpl
函数中, 整个渲染过程被 3 个函数分布实现:
这 3 个函数会处理fiber.flags
, 也会根据情况处理fiber.updateQueue.lastEffect
第一阶段: dom 变更之前, 处理副作用队列中带有Passive
标记的fiber
节点.
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
// ...省略无关代码, 只保留Hook相关
// 处理`Passive`标记
const flags = nextEffect.flags;
if ((flags & Passive) !== NoFlags) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
注意: 由于flushPassiveEffects
被包裹在scheduleCallback
回调中, 由调度中心
来处理, 且参数是NormalSchedulerPriority
, 故这是一个异步回调(具体原理可以回顾React 调度原理(scheduler)).
由于scheduleCallback(NormalSchedulerPriority,callback)
是异步的, flushPassiveEffects
并不会立即执行. 此处先跳过flushPassiveEffects
的分析, 继续跟进commitRoot
.
第二阶段: dom 变更, 界面得到更新.
function commitMutationEffects(
root: FiberRoot,
renderPriorityLevel: ReactPriorityLevel,
) {
// ...省略无关代码, 只保留Hook相关
while (nextEffect !== null) {
const flags = nextEffect.flags;
const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
case Update: {
// useEffect,useLayoutEffect都会设置Update标记
// 更新节点
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
// ...省略无关代码, 只保留Hook相关
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// 在突变阶段调用销毁函数, 保证所有的effect.destroy函数都会在effect.create之前执行
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
return;
}
}
}
// 依次执行: effect.destroy
function commitHookEffectListUnmount(tag: number, 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 {
if ((effect.tag & tag) === tag) {
// 根据传入的tag过滤 effect链表.
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
调用关系: commitMutationEffects->commitWork->commitHookEffectListUnmount
.
- 注意在调用
commitMutationEffects(HookLayout | HookHasEffect, finishedWork)
时, 参数是HookLayout | HookHasEffect
, 所以只处理由useLayoutEffect()
创建的effect
. - 根据上文的分析
HookLayout | HookHasEffect
是通过useLayoutEffect
创建的effect
. 所以commitMutationEffects
函数只能处理由useLayoutEffect()
创建的effect
. - 同步调用
effect.destroy()
.
第三阶段: dom 变更后
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
// ...省略无关代码, 只保留Hook相关
while (nextEffect !== null) {
const flags = nextEffect.flags;
if (flags & (Update | Callback)) {
// useEffect,useLayoutEffect都会设置Update标记
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
nextEffect = nextEffect.nextEffect;
}
}
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
// ...省略无关代码, 只保留Hook相关
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// 在此之前commitMutationEffects函数中, effect.destroy已经被调用, 所以effect.destroy永远不会影响到effect.create
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
schedulePassiveEffects(finishedWork);
return;
}
}
}
function commitHookEffectListMount(tag: number, 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 {
if ((effect.tag & tag) === tag) {
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
- 调用关系:
commitLayoutEffects->commitLayoutEffectOnFiber(commitLifeCycles)->commitHookEffectListMount
.
- 注意在调用
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork)
时, 参数是HookLayout | HookHasEffect
,所以只处理由useLayoutEffect()
创建的effect
. - 调用
effect.create()
之后, 将返回值赋值到effect.destroy
.
-
为
flushPassiveEffects
做准备-
commitLifeCycles
中的schedulePassiveEffects(finishedWork)
, 其形参finishedWork
实际上指代当前正在被遍历的有副作用的fiber
-
schedulePassiveEffects
比较简单, 就是把带有Passive
标记的effect
筛选出来(由useEffect
创建), 添加到一个全局数组(pendingPassiveHookEffectsUnmount
和pendingPassiveHookEffectsMount
).function schedulePassiveEffects(finishedWork: Fiber) { // 1. 获取 fiber.updateQueue const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); // 2. 获取 effect环形队列 const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { const { next, tag } = effect; // 3. 筛选出由useEffect()创建的`effect` if ( (tag & HookPassive) !== NoHookEffect && (tag & HookHasEffect) !== NoHookEffect ) { // 把effect添加到全局数组, 等待`flushPassiveEffects`处理 enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); enqueuePendingPassiveHookEffectMount(finishedWork, effect); } effect = next; } while (effect !== firstEffect); } } export function enqueuePendingPassiveHookEffectUnmount( fiber: Fiber, effect: HookEffect, ): void { // unmount effects 数组 pendingPassiveHookEffectsUnmount.push(effect, fiber); } export function enqueuePendingPassiveHookEffectMount( fiber: Fiber, effect: HookEffect, ): void { // unmount effects 数组 pendingPassiveHookEffectsMount.push(effect, fiber); }
-
综上commitMutationEffects
和commitLayoutEffects
2 个函数, 带有Layout
标记的effect
(由useLayoutEffect
创建), 已经得到了完整的回调处理(destroy
和create
已经被调用).
如下图:
其中第一个effect
拥有Layout
标记, 所以有effect.destroy(); effect.destroy = effect.create()
在上文commitBeforeMutationEffects
阶段, 异步调用了flushPassiveEffects
. 在这期间带有Passive
标记的effect
已经被添加到pendingPassiveHookEffectsUnmount
和pendingPassiveHookEffectsMount
全局数组中.
接下来flushPassiveEffects
就可以脱离fiber节点
, 直接访问effects
export function flushPassiveEffects(): boolean {
// Returns whether passive effects were flushed.
if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
const priorityLevel =
pendingPassiveEffectsRenderPriority > NormalSchedulerPriority
? NormalSchedulerPriority
: pendingPassiveEffectsRenderPriority;
pendingPassiveEffectsRenderPriority = NoSchedulerPriority;
// `runWithPriority`也是一个异步调用
return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
}
return false;
}
// ...省略无关代码, 只保留Hook相关
function flushPassiveEffectsImpl() {
if (rootWithPendingPassiveEffects === null) {
return false;
}
rootWithPendingPassiveEffects = null;
pendingPassiveEffectsLanes = NoLanes;
// 1. 执行 effect.destroy()
const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = ((unmountEffects[i]: any): HookEffect);
const fiber = ((unmountEffects[i + 1]: any): Fiber);
const destroy = effect.destroy;
effect.destroy = undefined;
if (typeof destroy === 'function') {
destroy();
}
}
// 2. 执行新 effect.create(), 重新赋值到 effect.destroy
const mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (let i = 0; i < mountEffects.length; i += 2) {
const effect = ((mountEffects[i]: any): HookEffect);
const fiber = ((mountEffects[i + 1]: any): Fiber);
effect.destroy = create();
}
}
其核心逻辑:
- 遍历
pendingPassiveHookEffectsUnmount
中的所有effect
, 调用effect.destroy()
.- 同时清空
pendingPassiveHookEffectsUnmount
- 同时清空
- 遍历
pendingPassiveHookEffectsMount
中的所有effect
, 调用effect.create()
, 并更新effect.destroy
.- 同时清空
pendingPassiveHookEffectsMount
- 同时清空
所以, 带有Passive
标记的effect
, 在flushPassiveEffects
函数中得到了完整的回调处理.
如下图:
其中拥有Passive
标记的effect
, 都会执行effect.destroy(); effect.destroy = effect.create()
假设在初次调用之后, 发起更新, 会再次执行function
, 这时function
只使用的useEffect
, useLayoutEffect
等api
也会再次执行.
在更新过程中useEffect
对应源码updateEffect, useLayoutEffect
对应源码updateLayoutEffect.它们内部都会调用updateEffectImpl
, 与初次创建时一样, 只是参数不同.
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 1. 获取当前hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
// 2. 分析依赖
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
// 继续使用先前effect.destroy
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 比较依赖是否变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 2.1 如果依赖不变, 新建effect(tag不含HookHasEffect)
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 2.2 如果依赖改变, 更改fiber.flag, 新建effect
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
updateEffectImpl与mountEffectImpl逻辑有所不同: - 如果useEffect/useLayoutEffect
的依赖不变, 新建的effect
对象不带HasEffect
标记.
注意: 无论依赖是否变化, 都复用之前的effect.destroy
. 等待commitRoot
阶段的调用(上文已经说明).
如下图:
- 图中第 1,2 个
hook
其deps
没变, 故effect.tag
中不会包含HookHasEffect
. - 图中第 3 个
hook
其deps
改变, 故effect.tag
中继续含有HookHasEffect
.
新的hook
以及新的effect
创建完成之后, 余下逻辑与初次渲染完全一致. 处理 Effect 回调时也会根据effect.tag
进行判断: 只有effect.tag
包含HookHasEffect
时才会调用effect.destroy
和effect.create()
当function
组件被销毁时, fiber
节点必然会被打上Deletion
标记, 即fiber.flags |= Deletion
. 带有Deletion
标记的fiber
在commitMutationEffects被处理:
// ...省略无关代码
function commitMutationEffects(
root: FiberRoot,
renderPriorityLevel: ReactPriorityLevel,
) {
while (nextEffect !== null) {
const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
}
}
在commitDeletion
函数之后, 继续调用unmountHostComponents->commitUnmount
, 在commitUnmount中, 执行effect.destroy()
, 结束整个闭环.
本节分析了副作用Hook
从创建到销毁的全部过程, 在react
内部, 依靠fiber.flags
和effect.tag
实现了对effect
的精准识别. 在commitRoot
阶段, 对不同类型的effect
进行处理, 先后调用effect.destroy()
和effect.create()
.