Que's Blog

WebFrontEnd Development

0%

react-17.0.2-hooks 之 useEffect

开头

这一篇主要是 useEffect的细节分析整理。因为 hooks 部分有点长也就分开了。关于 useState的部分可以看上一篇。这里入口函数的分析也在上一篇里面,如果有不明需要返回参考。

useEffect 细节

在常见的hooks 函数里面useEffect使用频度和 useState 是不相上下的。

使用场景

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';

export default () => {
const [text, setText] = useState('hello');

useEffect(() => {
console.log('Hello World');
}, [text]);

return <div onClick={() => setText('Hello')}>click</div>
}

以上是一个非常常见且典型的useEffect使用场景。

在这个使用例子中,useEffect 传入了一个函数作为第一个参数,传入一个state数组做为第二个参数。

细节

我们承袭前一片的分析,可以得知,当初次mount时候,useEffect 指向mountEffect函数,当 update 的时候,则指向updateEffect函数。

mountEffect

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
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
if (__DEV__) {
// 略
}

if (__DEV__ && enableDoubleInvokingEffects) {
// 略
} else {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
}
}
// 引用到的mountEffectImpl
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}

这里可以看出,在源码中,我们传入的函数称为 create,数组则称为 deps。

这里函数里面涉及到一些位运算的 flag,这里暂且略过对核心路径进行解读。

当这一路调用下来,通过mountWorkInProgressHook函数,它得到一个workInProgressHook,并在其memoizedState属性上挂载了一个新的空白Hook 实例。

接下来就是pushEffect函数。

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
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
// Circular
next: (null: any),
};
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;
}

如果前面对 useState那篇还有印象的话,大致可以看到这里主要是对updateQueue属性的处理,而 useState 则主要是对queue属性的处理。

这里还是简述一下过程

  • 当 hook上updateQueue属性没有有效值时候,创建一个空 updateQueue,并将它的lastEffect属性设置为由之前传入的 create 和 deps 构成的 Effect 实例,这个 Effect 是一个循环链表,这里将首尾都链到它自己。
  • 当有有效值时候将之前传入的 create 和 deps 构成的 Effect 实例,塞到firstEffect前面,并将它本身设为firstEffect。

updateEffect

这里看一下细节:

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 updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
if (__DEV__) {
// 略
}
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
// 调用的updateEffectImpl
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;

if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}

currentlyRenderingFiber.flags |= fiberFlags;

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

这里很显然就又回到了pushEffect,这里分支就是之前提到的

当有有效值时候将之前传入的 create 和 deps 构成的 Effect 实例,塞到firstEffect前面,并将它本身设为firstEffect。

这里处理的核心,归根结底,还是在于updateQueue属性。

PS: 当我写完这篇之后,自己再看一遍其实也还是感觉言之未尽。但是hooks这个机制呢,它本身说到底,其实还是针对函数组件中的各种状态进行一个临时的状态保存。

这里的保存,有闭包,也有直接的在属性上进行挂载。处理完毕这些之后在后续的更新里面对它进行读取、处理,并反馈到 View 上(说穿了其实有些兴味索然。。。)。

后续

pushEffect函数处理了hook.updateQueue的赋值,并将生成的 effect 赋值给到hook.memoizedState。

这里没有像 useState 那里那样,还有过一次直接计算和直接触发更新。究其根本,useEffect 在 mount 之后,并不一定真的会执行这个 create 函数,他就像一个订阅器,当我们执行了 setXXX(‘new Val’)之后,通过对应的 setXXX开始更新流程,并在这个流程中对比 deps,并针对性的执行 create。

然而这个流程,就是更新层面的逻辑了。回头看看很久之前分析过的update 环节,实在有些怅然。

《react-v16-Update renderPhase篇》

《react-v16-Update-commitPhase》

hooks 部分写到这里实在有些兴味索然,就这样做个收尾吧。这里的收获是,知道 fiber 节点上一些属性(updateQueue|memoizedState|pending之类),是如何变化、反馈到View层面的。