0%

react18.2函数组件加载过程

1. 加载过程

执行函数组件 Fiber 节点的 beginWork 工作,根据 tag 类型,进入 IndeterminateComponent 待定组件的逻辑处理【case IndeterminateComponent】:

每个函数组件的首次加载都是走的 IndeterminateComponent 分支逻辑,这是因为在创建函数组件 Fiber 的时候,react 没有更新它的 tag 值,所以它的首次 beginWork 工作就会进入 IndeterminateComponent 分支,在mountIndeterminateComponent 方法中才会更新它的 tag,使函数组件的Fiber在更新阶段执行 beginWork 时,能够进入正确的 FunctionComponent 分支。

1.1 mountIndeterminateComponent

packages\react-reconciler\src\ReactFiberBeginWork.js

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
function mountIndeterminateComponent(
_current,
workInProgress,
Component,
renderLanes,
) {

// 取出函数组件的props
const props = workInProgress.pendingProps;

// 存储FirstChild内容
let value;
let hasId;

# 调用函数组件
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderLanes,
);

// 针对类组件和函数组件进行不同的处理
if (
!disableModulePatternComponents &&
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
# 类组件的处理逻辑 【只是类组件现在已经不走这里了】

} else {

# 函数组件处理
// 更新tag为函数组件类型的值,下个逻辑就可以直接进入函数组件的处理【节点更新的时候】
workInProgress.tag = FunctionComponent;

# 创建子节点
reconcileChildren(null, workInProgress, value, renderLanes);

return workInProgress.child;
}
}

首先取出当前函数组件FIber节点上的最新的props,方便函数组件加载的使用,然后调用renderWithHooks方法,这个方法会执行我们定义的函数组件,返回值就是函数中return的内容,也就是jsx内容【处理过后的react-element元素对象】。

1.1.1 renderWithHooks

packages\react-reconciler\src\ReactFiberHooks.js

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
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
# 设置为当前渲染中的Fiber
currentlyRenderingFiber = workInProgress;

# 重置函数组件节点的数据
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;

// 设置首次加载的dispatcher【重点】
ReactCurrentDispatcher.current = current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

// Component为workInProgress.type 如果是函数组件:就是自身函数
# 调用这个函数,即调用组件,循环生成Element对象,
// 将return返回的Jsx内容转换为reactElement对象,最后返回这个对象
let children = Component(props, secondArg);
// 重置一些信息
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);

currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;

# 返回函数组件的内容【reactElement对象】
return children;
}
1.1.1.1 ReactCurrentDispatcher

ReactCurrentDispatcher 对象是一个全局变量,它是在react源码中的react包定义的:

packages\react\src\ReactCurrentDispatcher.js

1
2
3
4
5
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};

export default ReactCurrentDispatcher;

然后将它包装在一个新的对象中:

packages\react\src\ReactSharedInternalsClient.js

1
2
3
4
5
6
7
8
const ReactSharedInternals = {
ReactCurrentDispatcher,
ReactCurrentCache,
ReactCurrentBatchConfig,
ReactCurrentOwner,
};

export default ReactSharedInternals;

而shared包【通用工具包】会引入这个对象,然后暴露给全局:

packages\shared\ReactSharedInternals.js

1
2
3
import ReactSharedInternals from '../react/src/ReactSharedInternalsClient'

export default ReactSharedInternals;

其他资源包就可以通过shared工具包来拿到这个对象,所以我们在函数组件加载时才能使用这个对象:

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
import ReactSharedInternals from 'shared/ReactSharedInternals';
// 拿到ReactCurrentDispatcher对象
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;

知道了 ReactCurrentDispatcher 对象的由来,我们才能更好地理解它的作用,因为函数组件的每个 hook 实际就是在调用这个对象中的同名方法,比如 useState

packages\react\src\ReactHooks.js

1
2
3
4
export function useState(initialState){
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}

查看 resolveDispatcher:

1
2
3
4
5
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
// 返回获取到的dispatcher
return dispatcher;
}

其他的 hook 也是一样的原理。

1.1.1.2 函数组件调用

函数组件的加载核心就是执行一次函数的内容,理解起来也很简单。最后触发 return 关键字,这里的 jsx 内容会在 react 内部通过 jsxRuntime.jsx 方法进行处理,生成 react-element 对象,最后返回值就是创建的 react 元素对象。

最后返回生成的 react-element 对象,renderWithHooks 方法执行完成。

函数组件初始化执行完成后,就会更新函数组件Fiber节点的tag值为正确的类型FunctionComponent【后续逻辑函数组件节点便可以进入Function分支了】。

然后根据新建的value【react元素对象】创建子Fiber节点,最后返回子节点,函数组件的加载过程就基本完成了。

1.2 hooks的加载

根据上文得知,我们查看先前的 ReactCurrentDispatcher 对象

1
ReactCurrentDispatcher.current = HooksDispatcherOnMount

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const HooksDispatcherOnMount: Dispatcher = {
readContext,

useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState, // 加载state
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,

unstable_isNewReconciler: enableNewReconciler,
};

例如我们在函数中依次使用useState、useEffect、useLayoutEffect,我们来看一下执行过程:

1.2.1 mountState

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function mountState(initialState) {
# hook加载工作
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = {
pending: null, // 等待处理的update链表
lanes: NoLanes,
dispatch: null, // dispatchSetState方法
lastRenderedReducer: basicStateReducer, // 一个函数,通过action和lastRenderedState计算最新的state
lastRenderedState: initialState, // 上一次的state
};
hook.queue = queue;
const dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)
# 返回值
return [hook.memoizedState, dispatch];
}
1.2.1.1 mountWorkInProgressHook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function mountWorkInProgressHook(): Hook {
# hook对象
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};

if (workInProgressHook === null) {
// 第一个hook
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
# 后面的hook对象添加到第一个hook的next属性上,形成一个单向链表
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}

首先创建一个 hook 对象,workInProgressHook 默认为 null,它代表当前正在处理中的 hook 对象。

当前 useState 为函数组件中的第一个调用的 hook ,所以这时 workInProgressHook 肯定为 null

将新建 hook 对象赋值给 workInProgressHook,表示为正在处理中的 hook 对象。

同时也将第一个 hook 对象赋值给当前函数组件 Fiber 节点的 memoizedState 属性。

此时函数组件 Fiber 节点的 memoizedState 属性指向为:

alt text

继续回到 mountState 方法中:

hook 新建完成之后,判断传入的参数 initialState 是否为函数,如果为函数则调用此函数,将结果赋值为新的 initialState

然后设置 hook 对象的 memoizedStatebaseState 属性为初始的数据 initialState

接下来创建一个queue对象,这里要注意两个属性:

  1. lastRenderedReducer:它是一个函数,作用是根据action和lastRenderedState计算最新的state。
1
2
3
4
5
function basicStateReducer(state, action) {
// action就是setCount传入的参数,如果为一个函数,则将state传入进行计算,返回新的state
// 如果不是函数,则action就是最新的state
return typeof action === 'function' ? action(state) : action;
}
  1. lastRenderedState:代表上一次渲染的state

然后更新 hook 对象的 queue 属性,同时设置 queue 对象的 dispatch 属性为一个修改函数 dispatchSetState

最后返回一个数组,这就是 useState hook 的返回值:一个初始state和一个修改函数。

1.2.2 mountEffect

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
4
5
6
7
8
9
10
11
12
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// 进入effect加载
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect, # Passive标记对应的是useEffect
HookPassive,
create,
deps,
);
}
1.2.2.1 mountEffectImpl

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
# 创建的新的hook对象
const hook = mountWorkInProgressHook();
// 确定当前hook的deps依赖
const nextDeps = deps === undefined ? null : deps;
// 当前渲染中的Fiber节点,即函数组件对应的,打上effect钩子的flags
currentlyRenderingFiber.flags |= fiberFlags;
// 设置hook的memoizedState属性
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}

依然是先调用 mountWorkInProgressHook 创建一个 hook 对象,所以这里是先将第一个 hook 对象的 next 属性指向新建的 hook,然后再更新 workInProgressHook 的值为当前的 hook 对象。

此时函数组件Fiber节点的memoizedState属性指向为:

alt text

hook 创建完成之后,确定当前 hook 对象的 deps 依赖,因为我们传递的依赖为[],所以此时 deps 为一个空数组。然后更新当前 Fiber 节点的 flags 标记,最后设置 hook 对象的 memoizedState 属性内容,这里属性的结果为pushEffect 方法调用的返回值

1.2.2.2 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
28
29
30
31
32
33
34
function pushEffect(tag, create, destroy, deps) {
# 创建副作用对象
const effect = {
tag,
create, // 回调函数
destroy, // 销毁函数
deps,
// Circular
next: null,
};
// 取出当前函数组件的UpdateQueue
let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
if (componentUpdateQueue === null) {
// 为null时: 创建当前函数组件的UpdateQueue
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
// 第一个effect对象: 它的next属性会执行自己,形成一个单向环状链表
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 第二次加载其他的effect时: 将
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
# 上一个effect的next属性指向新建的effect
lastEffect.next = effect;
# 新建的next属性指向第一个effect
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}

首先创建了一个effect对象,查看它的内容:

alt text

  1. create 属性即为我们传入的回调函数。
  2. deps 属性是当前 useEffect hook的依赖。
  3. destory 属性为 undefined,它存储的是 useEffect hook返回的clean清理函数或者说销毁函数,但是它不是在这里赋值的,并且当前我们也没有返回这个函数。

然后取出当前函数组件 Fiber 节点的 updateQueue 属性内容赋值给变量 componentUpdateQueue

然后判断 componentUpdateQueue 是否为 null

null 时,然后调用 createFunctionComponentUpdateQueue 方法更新它的值。

1
2
3
4
5
6
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
return {
lastEffect: null,
stores: null,
};
}

最后将当前创建的 effect 对象的 next 属性指向了自身,且同时更新 updateQueue.lastEffect 属性为当前 effect 对象,由此形成一个单向环状链表。

所以此时函数组件 Fiber 节点的 updateQueue 属性更新为:

alt text

pushEffect 方法最后,返回当前创建的 effect 对象。

再回到 mountEffectImpl 中:

1
hook.memoizedState = pushEffect()

所以 hook 对象的 memoizedState 属性值为一个 effect 对象。

从这里我们可以发现,虽然每个hook对象都是相同的属性,但是不同的 hook 类型它存储的内容却完全不同。

  • useState 创建的 hook 对象,它的 memoizedState 属性存储的为数据 state
  • useEffect 创建的 hook 对象,它的 memoizedState 属性存储的为一个 effect 对象。

1.2.3 mountLayoutEffect

packages\react-reconciler\src\ReactFiberHooks.js

1
2
3
4
5
6
7
8
9
10
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
let fiberFlags: Flags = UpdateEffect;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStaticEffect;
}
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}

可以发现useEffect和useLayoutEffect共用了同一个加载方法 mountEffectImpl,所以它们会执行同样的逻辑处理。

hook 对象创建和处理,此时函数组件Fiber节点的memoizedState属性指向更新为:

alt text

effect 对象创建和处理,依然是 pushEffect 方法的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (componentUpdateQueue === null) {
...
} else {
# 第二次加载其他的effect时:
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
// 上一个effect的next属性指向新建的effect
lastEffect.next = effect;
// 新建的next属性指向第一个effect
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}

当前为第二个effect相关的 hook 处理,所以此时 Fiber.updateQueue【即componentUpdateQueue】是有值的,进入else分支处理。

更新Fiber.updateQueue.lastEffect属性指向为当前新建的 effect2,将 effect2next 属性指向为之前的 effect 对象。

此时函数组件 Fiber 节点的 updateQueue 属性指向更新为:

alt text

到此,函数组件加载阶段的 hooks 就处理完成。

1.3 commit阶段

前面全部的加载逻辑都是在 Fiber Reconciler 协调流程中执行的,即函数组件大部分的加载逻辑都是在 reconciler 协调流程中完成的【更新阶段同理】,还有剩下的一部分逻辑在 commit 阶段之中处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function commitRootImpl() {
// 发起调度处理useEffect回调
scheduleCallback(NormalSchedulerPriority, () => {
// 这个回调就是处理useEffect的
flushPassiveEffects();
});

// 1,BeforeMutation阶段
commitBeforeMutationEffects()
// 2,Mutation阶段,渲染真实DOM加载到页面
commitMutationEffects()
// 3,Layout阶段
commitLayoutEffects()
}

commit 阶段的内容都是同步执行,在进入具体的执行之前,都会先调用 scheduleCallback 方法发起一个新的调度,即创建一个新的任务 task,最后会生成一个新的宏任务来异步处理副作用【即执行useEffect的回调钩子】。

上面是 useEffect 的回调处理,我们再查看 useLayoutEffect 的回调处理。

1.4 Layout阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
function commitLayoutEffectOnFiber() {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
// 根据组件类型
switch (finishedWork.tag) {
// 函数组件的处理
case FunctionComponent: {
// 传入的是layout相关的flag标记
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
// ...
}
}
}

1.4.1 commitHookEffectListMount

packages/react-reconciler/src/ReactFiberCommitWork.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
# 当前函数组件的updateQueue属性,存储的是副作用链表
const updateQueue = finishedWork.updateQueue;
// 取出最后一个effect对象
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
// 获取第一个effect对象
const firstEffect = lastEffect.next;
let effect = firstEffect;
// 开始循环处理
do {
if ((effect.tag & flags) === flags) {
// Mount
const create = effect.create;
# 执行回调函数
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}

首先从当前函数组件 Fiber 节点取出它的 updateQueue 属性内容,在前面我们已经知道了 Fiber.updateQueue 存储的是副作用相关的链表,定义一个 lastEffect 变量存储 updateQueue.lastEffect 的内容,即最后一个 effect 对象。

判断 lastEffect 是否为 null ,如果 lastEffectnull,代表当前函数组件没有使用过 effect 相关的 hook

当前肯定是有值的,继续向下执行。从 lastEffect.next 中取出第一个 effect 对象,开始按顺序循环处理副作用。

1
2
3
4
5
6
7
8
9
do {
if ((effect.tag & flags) === flags) {
// Mount
const create = effect.create;
// 执行回调函数
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);

注意在执行之前有一个条件判断,只有存在 effect 相关的 flags 标记才会执行对应副作用回调。

而在之前 hook 加载是有进行设置的:

1
2
3
4
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags, // HookHasEffect标记就是表示有需要执行副作用
// ...
)

在函数组件加载阶段时,每个 useEffectuseLayoutEffect 都有打上 HookHasEffect 的标记,表示在加载阶段都会默认执行一次。

需要注意的是:之前 commitHookEffectListMount 传入的是与 Layout 相关的 flags 标记。

1
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); // Layout

所以这里只有 layout hook 的回调才能执行,第一个 effect 对象对应的是 useEffect,不满足判断条件

从当前 effect 对象的 next 属性取出下一个 effect 对象,开始第二次循环。

第二个 effect 对象对应的是 useLayoutEffect,满足判断条件,执行它的回调函数。

1
2
3
const create = effect.create;
// 执行回调函数
effect.destroy = create();

到此 hook 相关的回调处理完成,函数组件加载逻辑全部执行完成。

2. 更新过程

2.1 dispatchSetState

当操作更新触发 setState 时, 就是触发之前 useState 加载返回的 dispatch 方法:

1
2
3
const dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)
# 返回值
return [hook.memoizedState, dispatch];

packages\react-reconciler\src\ReactFiberHooks.js

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
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A, // state 1
) {

// 请求更新优先级
const lane = requestUpdateLane(fiber);
// 1. 创建update更新对象
const update: Update<S, A> = {
lane,
action, // state 1
hasEagerState: false,
eagerState: null,
next: (null: any),
};

if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
# 调度之前的一个优化策略校验: eagerState
// 快速计算出本次最新的state,与原来的进行对比,如果没有发生变化,则跳过后续的更新逻辑
const alternate = fiber.alternate;
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
// 当前的state,即旧的state
const currentState: S = (queue.lastRenderedState: any);
// 快速计算最新的state
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
# 如果state没变,组件不做更新。此处和useReducer对比下,useReducer还是会让函数组件更新
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
// nothing
}
}
}

// 2. 将更新对象入队
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
// 3. 开启一个新的调度更新任务
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
}

2.1.1 eagerState

首先看 dispatchSetState 方法的整个结构和类组件的更新方法 enqueueSetState 基本相同,还有 react 应用的初始加载 updateContainer,其实一个react应用的更新场景就只有这三种,而它们的更新逻辑就是以下几个步骤:

  1. 获取更新优先级 lane
  2. 创建 update 更新对象 。
  3. update 更新对象添加到目标Fiber对象的更新队列中。
  4. 开启一个新的调度更新任务。

它们的区别主要在于函数组件这里在调度之前有一个eagerState优化策略校验:

1
2
3
4
5
6
7
8
9
10
11
// 当前的state,即旧的state
const currentState: S = (queue.lastRenderedState: any);
// 快速计算最新的state
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
// 比较新旧state
if (is(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}

这个优化策略的作用是:调用 queue.lastRenderedReducer 方法,通过原来的 state 和当前传入的 action 参数,快速的计算出本次最新的 state 【即eagerState】,通过比较新旧 state 来判断数据是否变化,如果没有变化则可以跳过后续的更新逻辑,即不会开启新的调度更新任务。当前我们的 state 是有变化的,所以不满足优化策略,将继续向下执行更新。

2.1.2 enqueueConcurrentHookUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
export function enqueueConcurrentHookUpdate<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
lane: Lane,
): FiberRoot | null {
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
// 通用的入队方法
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
// 找到根节点并返回
return getRootForUpdatedFiber(fiber);
}

回到 dispatchSetState 方法中,最后还是会调用 scheduleUpdateOnFiber 函数进入更新的调度程序。

2.1.3 flushSyncWorkOnAllRoots

1
2
3
4
function processRootScheduleInMicrotask() {
// ...
flushSyncWorkOnAllRoots();
}

microtask 结束时,flush 任何 pending 的同步 work。这必须放在最后,因为它执行实际的可能会抛出异常的渲染工作。

直接快进到performSyncWorkOnRoot方法中:

1
2
3
4
export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
// ...
let exitStatus = renderRootSync(root, lanes);
}

调用renderRootSync方法,开始FiberTree的创建过程。

在这之前,还有一个处理要注意:把 concurrentQueues 的内容添加到 fiberqueue

1
2
3
4
5
6
7
8
function renderRootSync() {
// ...
prepareFreshStack()
}
function prepareFreshStack() {
// ...
finishQueueingConcurrentUpdates()
}

alt text

2.1.4 updateFunctionComponent

下面进入 beginWork 工作的 FunctionComponent 处理分支,开始函数组件的更新:

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
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes,
) {

let nextChildren;
# 调用函数组件
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);

# 函数组件默认的bailout策略,满足条件比较苛刻
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

// 创建子节点
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}

可以看见 updateFunctionComponent 方法主要有两个处理:

  1. 调用renderWithHooks【函数组件加载也是调用了这个方法】。
  2. 判断是否满足Bailout优化策略,满足则进入优化逻辑,跳过本组件的更新。不满足,则执行正常的组件更新逻辑。

2.1.5 renderWithHooks

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
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
// 设置为当前渲染中的Fiber
currentlyRenderingFiber = workInProgress;

// 重置函数组件节点的数据
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;

// 设置更新的dispatcher【重点】
ReactCurrentDispatcher.current =current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

// Component为workInProgress.type 如果是函数组件:就是自身函数
// 调用这个函数,即调用组件,循环生成Element对象,
// 将return返回的Jsx内容转换为reactElement对象,最后返回这个对象
let children = Component(props, secondArg);

renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);

currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;

# 返回函数组件的内容【reactElement对象】
return children;
}

在更新阶段时:

1
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate

renderWithHooks 方法的重点依然是组件的调用 Component(),这里的逻辑依然只是重新调用一遍我们定义的函数,最后返回最新的jsx内容【即reactElement对象】

2.1.6 hooks的更新

首先查看 useState 的更新:

1
2
3
const HooksDispatcherOnUpdate = {
useState: updateState, // 更新state
}
1
2
3
function updateState(initialState:) {
return updateReducer(basicStateReducer, initialState);
}
1
2
3
4
5
6
function updateReducer(reducer, initialArg, init?){
// 更新hook工作
const hook = updateWorkInProgressHook();

// ...
}

2.1.7 updateWorkInProgressHook

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
function updateWorkInProgressHook(): Hook {

// 即将处理的hook
let nextCurrentHook: null | Hook;
// 第一此进入更新时,currentHook为null
if (currentHook === null) {
// 取出当前正在更新的函数组件Fiber的旧节点
const current = currentlyRenderingFiber.alternate;
// 更新阶段,current都是存在的
if (current !== null) {
// 将旧节点的memoizedState 设置为下一个处理的Hook
// 将组件加载时,初始化的hook链表取出,memoizedState指向的是hook1
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 从第二个hook更新开始,会走这里
nextCurrentHook = currentHook.next;
}

// 设置下一个工作中的Hook为null
let nextWorkInProgressHook: null | Hook;
// 组件的第一个Hook更新时,workInProgressHook为null
if (workInProgressHook === null) {
// 将当前函数组件Fiber节点的memoizedState 设置为下一个处理的hook【默认是null】
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 如果不是第一个Hook,则取next指向的下一个
nextWorkInProgressHook = workInProgressHook.next;
}

// 下一个不为null, 说明当前hook不是最后一个更新的hook,只有最后一个hook更新时,nextWorkInProgressHook才为null
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;

currentHook = nextCurrentHook;
} else {

if (nextCurrentHook === null) {
throw new Error('Rendered more hooks than during the previous render.');
}

// 更新currentHook 为第一个hook
currentHook = nextCurrentHook;

// 创建一个新的Hook对象,复用原来的内容
const newHook: Hook = {
memoizedState: currentHook.memoizedState,

baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,

next: null, // 但是清空了next指向
};

// 第一个hook更新时,workInProgressHook为null,会进入这里
if (workInProgressHook === null) {
// This is the first hook in the list.
// 更新当前函数的组件的memoizedState为第一个hook对象,同时设置为当前正在工作中的hook
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
// 非第一个Hook,直接添加到上一个hook对象的next属性中
workInProgressHook = workInProgressHook.next = newHook;
}
}
// 返回当前正在工作中的hook
return workInProgressHook;
}

就像函数组件的hook在加载时都会调用一个 mountWorkInProgressHook 方法,生成一个hook链表。

而函数组件的hook在更新时也会调用一个 updateWorkInProgressHook 方法,生成对应的hook链表。

所以 updateWorkInProgressHook 方法的作用是:确定当前函数 Fiber 节点的 memoizedState 属性内容,也就是生成它的 hook 链表。

它的做法就是从 current 节点上取出函数组件加载时生成的 hook 链表,按顺序取出原来的 hook 对象,根据原来的对象信息创建生成新的 newHook 对象,最后按顺序一个一个添加到新的 Fiber 节点的 memoizedState 属性上。

注意:这里是一个重点,如果没有清空 next 属性,那更新当前函数组件 Fiber 节点的 memoizedState 属性,直接拿到第一个 hook 对象,就可以拿到整个 hook 链表,然后后续的 hook 更新就不需要再调用 updateWorkInProgressHook 方法了。

但是函数组件为啥不能如此处理呢?

因为react不能保证开发者是一定按照规范来使用的 hook ,如果开发者将 hook 置于条件语句中,在更新阶段出现了原来 hook 链表中不存在的 hook 对象,则在渲染时就会发生异常,所以react在函数组件更新时需要主动中断 hook 对象的next属性指向,按原来的链表顺序重新一个一个添加,如果出现了不匹配的 hook 对象,就会主动抛出异常,提示用户:

1
2
3
if (nextCurrentHook === null) {
throw new Error('Rendered more hooks than during the previous render.');
}

2.1.8 计算state

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
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 返回新的hook对象
const hook = updateWorkInProgressHook();
const queue = hook.queue;

if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}

queue.lastRenderedReducer = reducer; // 还是basicStateReducer,无变化
const current = currentHook; // 旧的hook对象,加载时useState创建的hook对象
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;

// The last pending update that hasn't been processed yet.
// 等待处理的更新链表:默认指向的是最后一个update对象
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// pendingQueue不为null,代表有需要处理的更新对象,然后需要将它们添加到baseQueue
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}

if (baseQueue !== null) {
// 我们有一个队列要处理
const first = baseQueue.next;
let newState = current.baseState;

let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;

# 循环处理update更新对象
do {
// An extra OffscreenLane bit is added to updates that were made to
// a hidden tree, so that we can distinguish them from updates that were
// already there when the tree was hidden.
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;

// Check if this update was made while the tree was hidden. If so, then
// it's not a "base" update and we should disregard the extra base lanes
// that were added to renderLanes when we entered the Offscreen tree.
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);

if (shouldSkipUpdate) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.

if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;

// Process this update.
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);


if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}

# 【重点】如果对某一个状态多次修改的最终结果是无变化,则会设置全局变量ReceivedUpdate为false,方便后续进入Bailout策略
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}

hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;

queue.lastRenderedState = newState;
}

const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}

updateWorkInProgressHook 方法调用完成之后,返回值就是 useState 对应的hook对象:

取出hook对象的queue队列,如果queue为null,则会抛出错误:

1
2
3
4
5
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}

后面的逻辑看似比较多,但其实比较简单,而且和this.setState计算state的逻辑基本一致。

它的核心逻辑:按顺序正向循环 update 更新队列,定义一个变量 newState 来存储最新的 state,然后根据原来 stateupdate 对象里面的信息计算最新的数据更新变量 newState,每循环一次就会从 update 对象的 next 属性取出下一个参与计算的 update,直接到所有的 update 处理完成。

当前 pendingQueue 结构【单向环状链表】:

alt text

在类组件中,会根据pendingQueue的内容重构生成一个新的单向链表,不再是环状,有明确的结束。

alt text

和类组件不同的是,函数组件这里并没有额外处理pendingQueue,而是直接复制给baseQueue,从baseQueue.next取出第一个update对象【即first】开始计算state。

所以函数组件这里的do while循环多了一个结束的判断条件,就是不能等于first,不然就会陷入无限循环:

1
2
3
do {
// ...
} while (update !== null && update !== first)

然后就是函数组件计算state的逻辑:

1
2
3
4
5
6
7
// do while循环中,计算state的核心逻辑
if (update.hasEagerState) {
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}

如果eagerState存在,则直接使用eagerState的值为新的state。

如果不存在,则调用reducer【basicStateReducer】,根据最新的newState和当前update对象的action重新计算state。

循环结束,在更新state之前,还有一个校验需要注意:

1
2
3
4
# 【重点】如果对某一个状态多次修改的最终结果是无变化,则会设置全局变量ReceivedUpdate为false,方便后续进入Bailout策略
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}

在针对一个状态的批量处理之后,有一个状态变化的校验,针对的是函数组件内部的 Bailout 策略。

即如果对某一个状态多次修改的最终结果是无变化,则会设置全局变量 ReceivedUpdatefalse,表示改组件没有更新的内容,这样就可以在 renderWithHooks 方法执行完成后,进入 Bailout 策略。

然后更新 hook 对象的 memoizedState 属性为最新的 newState

1
2
// 存储最新的state
hook.memoizedState = newState;

到此,useState hook 的更新程序执行完成,最后返回结果:

1
2
// 记忆state
return [hook.memoizedState, dispatch];

同时这里我们也可以明白:函数组件 useState hook 能够缓存变量结果的原因,因为它的 state 存储在 hook 对象的属性之中,并且这个属性可以在函数组件重新渲染过程中得到更新。

2.2 updateEffect

1
2
3
4
5
6
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(PassiveEffect, HookPassive, 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
24
25
26
27
28
29
30
31
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
// 取出新的依赖
const nextDeps = deps === undefined ? null : deps;
// 重置销毁方法
let destroy = undefined;

if (currentHook !== null) {
// 原来的pushEffect方法
const prevEffect = currentHook.memoizedState;
// 继承原来的destroy方法
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}

// 上面校验为true的情况下,这里就不会再执行
currentlyRenderingFiber.flags |= fiberFlags;

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

首先依然是调用一个 updateWorkInProgressHook 方法,前面已经详细讲解了它的作用。所以这里调用此方法后,就会新建一个 newHook 对象,添加到第一个 hook 对象的 next 属性之上,形成一个链表,后续如果还有新的 newHook 对象则继续执行同样的逻辑。

此时函数 Fiber 节点的 memoizedState 属性内容为:

alt text

2.2.1 areHookInputsEqual

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {

// 情况1,无依赖参数,每次渲染都会执行副作用
if (prevDeps === null) {
return false;
}

// 情况2,有至少一项依赖参数,循环判断每个依赖是否相等,任何一个依赖变化则会重新执行副作用
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
// 情况3,即空数组的情况,重新渲染不执行副作用
return true;
}

当前我们依赖为一个空数组,所以满足第三种情况,直接返回 true

  • 在依赖校验为 true 的情况下,即表示没有变化,此时更新 hook.memoizedState 属性,最后触发 return 关键字,updateEffect 方法执行完成。

  • 在依赖变化时,也会重新设置 hook.memoizedState 属性,依赖校验为true的情况下就是第一个参数不同,会打上 HookHasEffect 的标记。

    • 这个值会存储到 effect 对象的tag属性上,表示此effect对象有需要执行的副作用回调。
    • hookFlags 表示副作用的类型标记,比如 HookPassiveHookLayout
    • 所以依赖发生变化的唯一区别就是:打上了 HookHasEffect 标记。最终会 commit 阶段中执行回调时,判断 effect.tag 的值来判断是否执行回调。

到此,函数组件的第二个hook:useEffect更新完成。

2.3 updateLayoutEffect

1
2
3
4
5
6
function updateLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}

可以发现 useEffectuseLayoutEffect 共用了同一个更新方法 updateEffectImpl,所以它们会执行同样的逻辑处理。

  • 调用 updateWorkInProgressHook 方法:创建新 hook 对象,此时函数组件 Fiber 节点的 memoizedState 属性指向更新为:

alt text

  • 判断 deps 依赖是否变化,如果变化则为对应的 effect 对象打上 HookHasEffect 的标记。

到此,函数组件更新阶段的 hooks 就处理完成。

2.4 总结

函数组件更新阶段主要有这两个重点逻辑:

  • 根据 updateQueue 更新队列,循环计算 state,最后将最新的 state 数据存储到 Fiber.memoizedState 属性上并返回。
  • 更新 Effecthook 时,判断依赖是否变化打上 HookHasEffect,最后会根据 effect.tag 值来决定本次更新是否执行回调。

useEffectuseLayoutEffect的区别:useLayoutEffectuseEffect 的一个变种,它们都是在 React 组件中处理副作用的方法。两者之间的主要区别在于它们的执行时机。

  • useEffect 的回调函数会在每次渲染结束后异步执行,这意味着它不会阻塞浏览器的渲染过程。换句话说,React 会在处理 useEffect 内部的状态更新之前,让浏览器先绘制屏幕。
  • useLayoutEffect 的回调函数会在每次渲染结束后同步执行。这意味着它会阻塞浏览器的渲染过程,直到其执行完毕。因此,useLayoutEffect 可以在浏览器重新绘制屏幕之前触发。

需要注意的是,由于 useLayoutEffect 会阻塞浏览器的渲染过程,如果过度使用,可能会导致应用程序变慢,甚至引发性能问题。因此,在大多数情况下,应优先考虑使用 useEffect。只有在某些特定情况下,例如需要在 DOM 更新后立即进行某些操作,或者需要在用户看到渲染结果之前进行某些操作,才考虑使用 useLayoutEffect