0%

react18.2hook之useEffect和useLayoutEffect原理

useEffect

初始化

先来解读下几个参数:

  • fiberFlags:有副作用的更新标记,用来标记hook所在的fiber;
  • hookFlags:副作用标记;
  • create:使用者传入的回调函数;
  • deps:使用者传入的数组依赖;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function mountEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = mountWorkInProgressHook();
// 判断是否有传入deps,如果有会作为下次更新的deps
const nextDeps = deps === undefined ? null : deps;
// 给hook所在的fiber打上有副作用的更新的标记
currentlyRenderingFiber.flags |= fiberFlags;
// 将副作用操作存放到fiber.memoizedState.hook.memoizedState中
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
createEffectInstance(),
nextDeps,
);
}

function createEffectInstance(): EffectInstance {
return {destroy: undefined};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function pushEffect(
tag: HookFlags,
create: () => (() => void) | void,
inst: EffectInstance,
deps: Array<mixed> | null,
): Effect {
// 初始化副作用结构
const effect: Effect = {
tag,
create,
inst,
deps,
// Circular
next: (null: any),
};
// 向fiber的updateQueue上添加effect,并形成环形链表
let componentUpdateQueue: null | FunctionComponentUpdateQueue =
(currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
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;
}
}

return effect;
}

上面这段代码除了初始化副作用的结构代码外,还有就是操作闭环链表,向链表末尾添加新的 effect,该 effect.next 指向 fisrtEffect,并且链表当前的指针指向最新添加的 effect

简单总结一下:给 hook 所在的 fiber 打上副作用更新标记,并且 fiber.memoizedState.hook.memoizedStatefiber.updateQueue 存储了相关的副作用,这些副作用通过闭环链表的结构存储。

alt text

更新

主要功能就是创建一个带有回调函数的 newHook 去覆盖之前的 hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function updateEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const effect: Effect = hook.memoizedState;
const inst = effect.inst;

// currentHook is null on initial mount when rerendering after a render phase
// state update or for strict mode.
if (currentHook !== null) {
if (nextDeps !== null) {
const prevEffect: Effect = currentHook.memoizedState;
const prevDeps = prevEffect.deps;
// 比较两次依赖数组中的值是否有变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
return;
}
}
}

currentlyRenderingFiber.flags |= fiberFlags;

hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
inst,
nextDeps,
);
}

alt text

create 和 destroy 函数是怎样被调用?

在弄清楚怎样被调用之前,我们先来看 destroy 函数的存在性问题。即:在 hookmount 阶段,我们创建 effect 对象的时候,destroy 函数是不存在的。因为,destroy 函数本来就是 create 函数的返回值。而此时 create 函数还没有被调用。

而我们知道,组件 mount 阶段过后, create 函数是一定会被调用的。所以,我们也可以推理得出,在组件的 update 阶段,effect 对象的 create 函数和 destroy 函数肯定是存在的(现在我们假设用户一定定义 destroy 函数 )。

react 会在 commit 阶段去调用我们的 create 函数和 destroy 函数。commit 阶段又可以分为三个子阶段:

  • beforeMutation
  • mutation
  • layout

回到源码的视角,create 函数和 destroy 函数调用具体是发生在 commit 阶段的入口函数 commitRootImpl 内部,而真正的调用入口函数为 flushPassiveEffects

调用入口

该函数在 commitRootImpl 出现了三次。也就是说 create 函数和 destroy 函数调用存在三个入口:

  1. 处理由于调用 flushSyncUpdateQueue() 所衍生的 effect
  2. beforeMutation 子阶段之前的对执行 effect 进行调度
  3. layout 子阶段完成之后的同步调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
didIncludeRenderPhaseUpdate: boolean,
renderPriorityLevel: EventPriority,
spawnedLane: Lane,
) {
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);

// ...

if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
// workInProgressTransitions might be overwritten, so we want
// to store it in pendingPassiveTransitions until they get processed
// We need to pass this through as an argument to commitRoot
// because workInProgressTransitions might have changed between
// the previous render and commit if we throttle the commit
// with setTimeout
pendingPassiveTransitions = transitions;
scheduleCallback(NormalSchedulerPriority, () => {
// ! 1. 异步执行 passive effects
flushPassiveEffects();
// This render triggered passive effects: release the root cache pool
// *after* passive effects fire to avoid freeing a cache pool that may
// be referenced by a node in the tree (HostRoot, Cache boundary etc)
return null;
});
}
}

const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;

if (subtreeHasEffects || rootHasEffect) {

// ! 2. 进入commit阶段
executionContext |= CommitContext;

const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);

// ...

// ! 3. mutation阶段 (包括DOM变更)
commitMutationEffects(root, finishedWork, lanes);

// ...

// ! 4. layout阶段
commitLayoutEffects(finishedWork, root, lanes);
}
// ...
if (includesSyncLane(pendingPassiveEffectsLanes) && root.tag !== LegacyRoot) {
flushPassiveEffects();
}

// ...
}

通过简单的 react 应用来测试得到的结果是:1)在组件的 mount 阶段,react 会走第二个入口;2)在组件的 update 阶段,react 会走第三个入口

不管是从哪个入口进入,create 函数和 destroy 函数调用都是发生了 DOM 更新之后

不管是从哪个入口进入,它们都是走同一个函数 flushPassiveEffects

1
2
3
4
5
6
7
8
9
10
11
function flushPassiveEffectsImpl() {
// ...

const prevExecutionContext = executionContext;
executionContext |= CommitContext;

commitPassiveUnmountEffects(root.current); // cleanup
commitPassiveMountEffects(root, root.current, lanes, transitions); //setup

// ...
}

且有相同的调用栈:

alt text

因为调用 destroy 和 调用 create 函数是分开的。所以,我们需要将两者分开来讨论。但是由于两者的调用逻辑几乎是一样的。所以,在这里,我们以调用 destroy 函数为例即可,create 函数的调用原理跟这个是一样的。
通过源码的阅读和调试,我将「调用 destroy 函数的过程」划分为两个步骤:

  1. 遍历 fiber 树 - 深度优先遍历 fiber 树,找到身上有 effectfiber 节点
  2. 遍历 effect 链表 - 遍历当前的 effect 链表,根据当前 effect 是否满足特定的条件(是否包含特定标签)来确定是否要调用 destroy 函数。

1. 遍历 fiber 树

我们也知道,所有的 useEffect hook 函数只能用 function component 里面使用。

fiber 上可能存在多个 function component 类型的 fiber 节点使用了 hook 函数。所以,这一步中,遍历 fiber 树的目的就是要找到消费了 hook 函数的 fiber 节点。

在这一步之前,react 其实是有做一些前置工作的。那就是:因为遍历 fiber 都是深度优先,在 render 阶段,performUnitOfWork 中的 beginWork 执行到叶子节点后会进入到 completeUnitOfWork 中,会执行 bubbleProperties,也就是 react 会逐步向上追溯当前消费了 hook 函数的 fiber 节点的所有的祖先 fiber 节点,一一给它们的 subtreeFlags 属性值加入一个 Passive 标签。这种机制类似于浏览器的事件冒泡。浏览器的事件起源于特定的 DOM 节点,但是它会在冒泡阶段向上传播到 document 这个根节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// packages\react-reconciler\src\ReactFiberCompleteWork.js
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
// ...
}
}

function bubbleProperties(completedWork: Fiber) {
const didBailout =
completedWork.alternate !== null &&
completedWork.alternate.child === completedWork.child;

let newChildLanes = NoLanes;
let subtreeFlags = NoFlags;

if (!didBailout) {
// ? sy
// Bubble up the earliest expiration time.
// “向上冒泡”最早的过期时间
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// In profiling mode, resetChildExpirationTime is also used to reset
// profiler durations.
let actualDuration = completedWork.actualDuration;
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
// ! 遍历completedWork的所有子节点
let child = completedWork.child;
while (child !== null) {
// ! 1. 将他们的lanes和childLanes合并到newChildLanes中
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
// ! 2. 将他们的subtreeFlags和flags合并到subtreeFlags中
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;

actualDuration += child.actualDuration;

// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}

completedWork.actualDuration = actualDuration;
completedWork.treeBaseDuration = treeBaseDuration;
} else {
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);

subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;

// Update the return pointer so the tree is consistent. This is a code
// smell because it assumes the commit phase is never concurrent with
// the render phase. Will address during refactor to alternate model.
child.return = completedWork;

child = child.sibling;
}
}
// ! 子树的flags
completedWork.subtreeFlags |= subtreeFlags;
} else {
// Bubble up the earliest expiration time.
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// In profiling mode, resetChildExpirationTime is also used to reset
// profiler durations.
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);

let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);

// "Static" flags share the lifetime of the fiber/hook they belong to,
// so we should bubble those up even during a bailout. All the other
// flags have a lifetime only of a single render + commit, so we should
// ignore them.
subtreeFlags |= child.subtreeFlags & StaticMask;
subtreeFlags |= child.flags & StaticMask;

// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}

completedWork.treeBaseDuration = treeBaseDuration;
} else {
let child = completedWork.child;
// ! 遍历completedWork的所有子节点
while (child !== null) {
// ! 1. 将他们的lanes和childLanes合并到newChildLanes中
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);

// "Static" flags share the lifetime of the fiber/hook they belong to,
// so we should bubble those up even during a bailout. All the other
// flags have a lifetime only of a single render + commit, so we should
// ignore them.
// “静态”标志(Static flags)与它们所属的 Fiber 或 Hook 共享生命周期,因此即使在放弃(bailout)时,也应该将这些flags向上冒泡。
// 而其他所有flags仅在单次render + commit 的生命周期内存在,因此我们应该忽略它们。
// ! 2. 将他们的(subtreeFlags&StaticMask)和flags合并到subtreeFlags中
subtreeFlags |= child.subtreeFlags & StaticMask;
subtreeFlags |= child.flags & StaticMask;

// Update the return pointer so the tree is consistent. This is a code
// smell because it assumes the commit phase is never concurrent with
// the render phase. Will address during refactor to alternate model.
// 更新return pointer以保持树的一致性。这被描述为一种代码异味(code smell),因为它假设commit阶段永远不会与 render 阶段并发。
// 在重构为交替模型(alternate model)时将会解决这个问题。
child.return = completedWork;

child = child.sibling;
}
}

completedWork.subtreeFlags |= subtreeFlags;
}
// ! 后代节点的lanes
completedWork.childLanes = newChildLanes;

return didBailout;
}

回到 useEffect 这个主题。上面所提到的前置工作中,有一点特别需要注意的点是:如果「消费了 hook 函数的那个 fiber 节点」的子树之下没有其他消费了 hook 函数的 fiber 节点,它自己的 subtreeFlags 属性值是不会被贴上一个 Passive 标签的。这也不难理解,因为这恰恰是符合 subtreeFlags 属性名的语义的。

假设只有我们的 <Counter /> 组件里面消费到 useEffect 这个 hook 函数。那么,在 render 阶段,经过上面所提到的前置工作后,我们会得到这样的一颗 fiber 树:

alt text

好了,做完上面的前置工作后,react 会在 commit 阶段发起对 flushPassiveEffects 的调度,将任务加入 taskQueue 中,在适当的时间切片内会执行到此调度。

这个遍历工作是从 fiber 树的根节点 - hostRootFiber 开始的。

源码中的 commitPassiveUnmountOnFiber()recursivelyTraversePassiveUnmountEffects()commitHookPassiveUnmountEffects() 这三个函数共同完成了这个过程。而这三个函数是有分工的:

  • commitPassiveUnmountOnFiber() - 是遍历流程的入口
  • recursivelyTraversePassiveUnmountEffects() - 负责实现了以深度优先的算法的「递」与「归」
  • commitHookPassiveUnmountEffects() - 在「归」之前,尝试去调用当前 fiber 节点 effect 链表上的所有的 destroy 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function my_commitPassiveUnmountOnFiber(parentFiber){
if (parentFiber.subtreeFlags & PassiveMask) {
const child = parentFiber.child;

while (child !== null) {
my_commitPassiveUnmountOnFiber(child);
child = child.sibling;
}
}

const isfunctionComponentKind = [FunctionComponent, ForwardRef, SimpleMemoComponent].includes(parentFiber.tag);
const hasPassiveEffect = parentFiber.flags & Passive

if(isfunctionComponentKind && hasPassiveEffect){
commitHookPassiveUnmountEffects(
parentFiber,
parentFiber.return,
Passive | HasEffect
);
}
}

其实看完 react 源码,熟悉 react 内部的原理后,这种深优先遍历算法跟 render 阶段的 「work-loop」的遍历算法是一模一样的。唯一的一个区别点有两点:

  1. 它深度优先遍历不一定会遍历到当前子树路径的叶子节点。它会因为不满足「以当前 parentFiber 为根节点的子树上是否有身上贴有 Passive 标签的 fiber 节点?」这个条件提前终止了;
  2. render 阶段的 「work-loop」的遍历算法中,归去之前必定会执行 complete work 不同,当前的遍历算法会先检查是否满足条件,如果不满足,则不执行 “complete work” 。显然,这里的 “complete work” 是指 commitHookPassiveUnmountEffects() 的调用。

2. 遍历 effect 链表

上面,我们已经介绍了 react 是如何了遍历整棵 fiber 树,找到那些所有需要执行 effectfiber 节点。上一步骤末尾,我们也指出了,执行 effectfiber 节点的函数是 commitHookPassiveUnmountEffects()。而 commitHookPassiveUnmountEffects() 会直接调用 commitHookEffectListUnmount()。顾名思义,该函数就是 react 遍历 effect 链表去调用 destroy 函数的所在。

effect 链表并不是只存储 useEffect hook 函数的 effect 对象

那这里所提到的 「effect 类型的」hook 函数有哪些呢?从 react@18.2.0 的源码来看,它包括了5 种类型的 hook 函数:

  • useEffect()
  • useLayoutEffect()
  • useInsertionEffect()
  • useSyncExternalStore()
  • useImperativeHandle()

提出这个结论的依据是: 这些 hook 函数的调用栈最终都是指向了 pushEffect() 函数。如下图:

alt text

pushEffect() 函数就是负责创建新的 effect 对象,把它追加到当前 fiber 节点 effect 链表的尾部。可见,如果一个 function component 都消费了上面这些 hook 函数的话,那么它所对应的 fiber 节点的 effect 链表上并不是单纯包含 useEffect hook 函数所产生的 effect
回到正题。上面我们指出了 commitHookEffectListUnmount 函数就是 react 遍历 effect 链表去调用 destroy 函数的所在,下面来看看它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function commitHookEffectListUnmount(
flags,
finishedWork,
nearestMountedAncestor
) {
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) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;

if (destroy !== undefined) {
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
}
}

effect = effect.next;
} while (effect !== firstEffect);
}
}

function safelyCallDestroy(current, nearestMountedAncestor, destroy) {
try {
destroy();
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
}
}

有了上面的铺垫,我们很快就能看明白这段代码的意思。react 通过 do{...}while() 循环来遍历 effect 链表。我们在上面也提到过,effect 链表其实是单向的循环链表。所以,当前即将需要遍历的 effect 对象又指会了第一个 effect 的时候,意味着我们已经遍历完了整条链表,可以退出循环了。

小结

alt text

执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {useEffect} from "react";

function Child(){
useEffect(function childEffect(){
console.log('Child effect been called');
});

return null
}

function Sibling(){
useEffect(function siblingEffect(){
console.log('Sibling effect been called');
});

return null;
}

function App(){
useEffect(function appEffect(){
console.log('App effect been called');
});

return (
<div>
<Child />
<Sibling />
</div>
)
}

相信看完上面的分析,也不难得出答案:

1
2
3
Child effect been called
Sibling effect been called
App effect been called

react 采用的递归遍历算法是深度优先算法。示例 react 应用所对应的 fiber 树如下:

alt text

用文字描述整个递归遍历过程是这样的:
<App> 的递;
<div> 的递;
<Child> 的递;
<Child> 的归;
<Sibling> 的递;
的归; ``<App> 的归。

在归时候,react 会在当前 fiber 节点上调用 commitHookEffectListMount() 函数,所以,调用结果是:

<Child> 身上调用 commitHookEffectListMount()
<Sibling> 身上调用 commitHookEffectListMount()
<App> 身上调用 commitHookEffectListMount()

useLayoutEffect

useLayoutEffect 函数跟 useEffect hook 函数 的不同。两者的不同之处在于两点:

  1. 它们给「自己所创建的 effect 对象」所贴的 effect flag 是不同的;
  2. 它们给「自己所关联的 fiber 节点」所追加的 fiber flag 的不同的。

所贴的 hook flag 不同

  • 对于 useLayoutEffect() hook 函数而言,它给自己所创建的 effect 对象所贴的 effect flagLayout
  • useEffect() hook 函数给自己所创建的 effect 对象所贴的 effect flagPassive

所追加的 fiber flag 不同

对于 useEffect() hook 函数而言,它给自己所关联的 fiber 节点所追加的 fiber flag 是:

  • mount 阶段是 Passive | PassiveStatic
  • update 阶段是 Passive

useLayoutEffect() hook 函数给自己所关联的 fiber 节点所追加的 fiber flag 是:

  • mount 阶段是 Update | LayoutStatic
  • update 阶段是 Update

useLayoutEffect - mutation 子阶段调用 destroy 函数

它的 destroy 函数的调用是发生在 mutation 子阶段。具体来讲,就是发生在 commitMutationEffectsOnFiber() 这个函数里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;

switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);

if (flags & Update) {
// ......
{
try {
commitHookEffectListUnmount(
Layout | HasEffect,
finishedWork,
finishedWork.return
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}

return;
}
// ......
}
}

useLayoutEffect - layout 子阶段调用 create 函数

  • hook 类型 - 这里就是指 useLayoutEffect() hook 的 create 函数;
  • class 类型 - 这里是指 class component 的两个生命周期函数:
    • componentDidMount()
    • componentDidUpdate()

useLayoutEffect() hookcreate 函数的真正调用入口为 commitHookLayoutEffects()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function commitLayoutEffectOnFiber(
finishedRoot,
current,
finishedWork,
committedLanes
) {
const flags = finishedWork.flags;

switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes
);

if (flags & Update) {
commitHookLayoutEffects(finishedWork, Layout | HasEffect);
}

break;
}

// ......
}
}

应用

关于这个问题,react 官方文档给出了一个使用用例: 在浏览器重绘之前测量 DOM 的布局信息

「界面更新时候的闪烁问题」。为什么使用 useLayoutEffect() 能解决这类的问题呢?

首先,我们要知道为什么会造成闪烁。造成闪烁的原因是 react 在短时间更新了多帧显示内容不一样的界面。而 useLayoutEffect() hookcreate 函数是在 layout 子阶段以同步的,批量的方式去执行的。也就是说,create 函数里面的所发起的多次状态更新请求只会产生一次的实质性的界面重绘。 通过抹除代表着中间状态的过渡帧,将多帧压缩为一帧来更新界面,这就是 useLayoutEffect() 能解决「界面更新时候的闪烁问题」原因之所在。

上面所提到的 「界面更新时候的闪烁问题」只是 useLayoutEffect() 能解决问题中的一个垂类。useLayoutEffect() 应该还能解决更多不同业务场景下的问题,我们得通过现象看本质。

useLayoutEffect() 的本质能力是什么?

  • 访问更新后的 DOM 树 - 因为 useLayoutEffect() hookcreate 函数是在 mutation 子阶段之后的 layout 子阶段执行的。所以,这就意味着我们可以去访问更新后的 DOM 树和做一些布局信息的测量。
  • 同步/批量执行状态更新,阻塞浏览器重绘 - react 将会以「同步阻塞,批量更新」的方式去对待 create 函数体里面所发起的过个状态更新请求。也就说,在 create 函数体里面所发起的过个状态更新请求只会产生一次界面重绘。

总结

从源码的角度来看, 这两者其实都是在同一个架构里面,拥有很多相同点:

  • 每一个 effect 类型 hook 都会关联一个 hook 对象 和 effect 对象;
  • 同一个 function component 内,所有的 hook 对象都共用同一条 hook 链表;
  • 同一个 function component 内,所有的 effect 对象都共用同一条 effect 链表;
  • 同一个调用机制 - 在 render 阶段创建 hook 对象和 effect 对象,在 commit 阶段去调用 hookcreatedestroy 函数。

这两者的不同点在于:

  • useEffect()destroycreate 函数是在同一个子阶段(layout 子阶段后的 passive 子阶段)调用的(调用入口要么是调度后的 flushPassiveEffectImpl(),要么是同步的flushPassiveEffectImpl());
  • useLayoutEffect() 的这两个函数却是在不同的子阶段执行。destroy 是在 mutation 子阶段执行的,而 create 是在 layout 子阶段。