通俗易懂的React原理(三):函数组件和hooks原理

代码以React v19.1.0为例,https://github.com/facebook/react/tree/v19.1.0

上一篇我们讲了React构建fiber树的整体流程,即循环调用performUnitOfWork,里面穿插着beginWork和completeWork的调用,并且最后给出了简洁的伪代码实现。

那么这一篇呢,我们先看一下beginWork前半部分的逻辑。因为beginWork它的主要工作是渲染组件(此处的渲染即,我们平时所说的渲染,对于函数组件来说就是执行),并且根据返回的jsx,生成子节点。说到了渲染组件,我觉得这肯定是很多人最感兴趣的了。对于这么重量级的部分,我认为有必要牺牲一下学习的连贯性,把他吃透,这样反而更有利于加深我们对React的理解。

这一篇篇幅会很长,我会从beginWork入手,讲到React的hooks实现,并且以useReducer为例,讲一下React Hooks实现的原理,讲完了之后,下一篇再回来继续看beginWork的部分。

beginWork里面的逻辑

beginWork

先看beginWork代码,依旧是去掉开发环境下的代码,和Profiler相关的代码。然后它里面有个switch case的逻辑,分支太多了,我就保留几个意思意思吧,想了解更多可以自己去看源码

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
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;

if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else {
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
(workInProgress.flags & DidCapture) === NoFlags
) {
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;

if (getIsHydrating() && isForkedChild(workInProgress)) {
const slotIndex = workInProgress.index;
const numberOfForks = getForksAtLevel(workInProgress);
pushTreeId(workInProgress, numberOfForks, slotIndex);
}
}

workInProgress.lanes = NoLanes;

switch (workInProgress.tag) {
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
disableDefaultPropsExceptForClasses ||
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps = resolveClassComponentProps(
Component,
unresolvedProps,
workInProgress.elementType === Component,
);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case Throw: {
// This represents a Component that threw in the reconciliation phase.
// So we'll rethrow here. This might be a Thenable.
throw workInProgress.pendingProps;
}
}

删减了很多,但是依旧是很长。前面一大段不重要啊,就是判断这个组件是否需要重新渲染的。如果不需要重新渲染,就跳过了。值得提一嘴的就是前面那个current!==null,就是判断是初次渲染(mount),还是重新渲染(update)。因为初次渲染是没有current节点的,这一点前面我们讲current tree和workInProgress tree关系的时候,没有详细说,所以这次特别提一下。

直接看下面switch case那一段,就是根据fiber node的各种tag,去执行不同的操作。比如,对于函数组件,就去执行updateFunctionComponent这个方法。我们给他传入的入参,分别是current节点,workInProgress节点,函数组件本身(没错,对于函数组件,fiber节点的type字段就是这个函数本身,你可以随便本地起个React项目打断点看一下),组件的props,和这个组件的渲染优先级。

updateFunctionComponent

看看对于函数组件干了什么,依旧是精简后的代码

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
function updateFunctionComponent(
current: null | Fiber,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
let context;
if (!disableLegacyContext && !disableLegacyContextForFunctionComponents) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
context = getMaskedContext(workInProgress, unmaskedContext);
}

let nextChildren;
let hasId;
prepareToReadContext(workInProgress, renderLanes);

nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();

if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}

// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}

前面几行依旧不重要,是处理context 相关的。其实这个方法里最重要的就两个函数调用,一个是renderWithHooks,一个是reconcileChildren。它们分别完成了渲染组件,和生成子节点的任务。关于reconcileChildren,我们要很久之后再见了。接下来很大的篇幅,包括后面几篇,我都会讲renderWithHooks,和hooks的具体实现相关的内容。

renderWithHooks

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
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;
currentlyRenderingFiber = workInProgress;

workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;

ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

let children = Component(props, secondArg);

if (didScheduleRenderPhaseUpdateDuringThisPass) {
children = renderWithHooksAgain(
workInProgress,
Component,
props,
secondArg,
);
}

finishRenderingHooks(current, workInProgress, Component);

return children;
}

简单总结,就是首先把这个fiber节点的一些字段都清空,然后根据是mount还是update,分别给所有的hooks赋值上相应的实现。

然后就去执行这个函数,这个其实我们之前平时写React就知道,函数组件的渲染就是执行它本身嘛。你在函数组件里面写的代码,全会在此时被执行。

然后如果组件渲染过程中,又产生了新的更新,比如在函数体里面setState(虽然这是不对的),那么react会再重新render一遍,直到组件稳定了,不会在渲染过程中产生新的更新,或是重复调用太多次而报错。

最后,还原一下上下文,等着下一个组件来渲染。

我们可以先看一下给hooks添加上实现的那一段,也就是

1
2
3
4
ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

我们直接去packages/react目录下搜索一下hooks的实现,比如useReducer,能看到它的实现其实是resolveDispatcher().useReducer(reducer, initialArg, init)。而这个resolveDispatcher,其实就是直接返回了ReactSharedInternals.H。这个变量是定义在shared包里的,供React各个子包一起使用的。

HooksDispatcherOnMountHooksDispatcherOnUpdate,分别就是mount过程中和update过程中,全部hooks的实现。

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
const HooksDispatcherOnMount: Dispatcher = {
readContext,

use,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
useHostTransitionStatus: useHostTransitionStatus,
useFormState: mountActionState,
useActionState: mountActionState,
useOptimistic: mountOptimistic,
useMemoCache,
useCacheRefresh: mountRefresh,
};

const HooksDispatcherOnUpdate: Dispatcher = {
readContext,

use,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useSyncExternalStore: updateSyncExternalStore,
useId: updateId,
useHostTransitionStatus: useHostTransitionStatus,
useFormState: updateActionState,
useActionState: updateActionState,
useOptimistic: updateOptimistic,
useMemoCache,
useCacheRefresh: updateRefresh,
};

Hooks的具体实现

我们在上面renderWithHooks里面看到了let children = Component(props, secondArg);这行代码,它会渲染我们的函数组件,即执行函数本身。执行的时候,·你在函数组件里面写的各种hooks,就被替换成上面hooks的具体实现来执行。

那么我们这一篇,就以useReducer为例,来看一看Hooks的原理和具体实现,看看它在组件渲染的过程中做了什么。选useReducer是因为,最常用的Hooks是useState,而useState是useReducer的一种特殊场景,相信看到这里的也都熟悉useReducer。所以我们选择更具通用性的useReducer来研究(而且useState完全是通过调用useReducer来实现的)。

mountReducer

当函数组件首次渲染,即mount的时候,对于组件里的useReducer hook,执行的就是mountReducer。简单来讲,就是把hook挂到fiber上面

我们来看看mountReducer具体是怎么实现的

1
2
3
4
5
6
7
8
9
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
//太长啦等下再看
return [hook.memoizedState, dispatch];
}

首先第一行就是一个看名字就很重要的函数调用,mountWorkInProgressHook。那我们不得不再去看看它的实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null, // 当前实际的state(即useReducer返回的第一个参数)

baseState: null, // 如果有低优先级更新被打断,记录被打断之前的state
baseQueue: null,// 如果有低优先级更新被打断,则从这个更新开始,记录后续所有的更新
// 和baseState一起,用于当低优先级更新被恢复执行的时候,基于baseState,执行 // baseQueue上所有的更新,从而计算出正确的状态
queue: null, // 待执行的更新相关的内容

next: null, // fiber节点的下一个hook
};

if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}

还好它并不长,他只是定义了一个新的hook对象,每个字段的含义我都在注释里写了。其中baseState和baseQueue如果看不懂的话可以先略过,这个和React的并发更新有关,后面会细讲,不用现在死磕。

然后workInProgressHook是一个全局对象,是一个链表。它会在合适的时候被清空。如果它是null,那就代表当前这个Fiber节点上还没有hook,那么我们就要把hook挂到Fiber节点的memoizedState字段上。

注意,这里的memoizedState和上面hook对象里的memoizedState,并没有关系,只是同名而已。一个代表Fiber的hooks链表,一个代表hook的state。

我们以一个很简单的react组件举例子,它用了两个useReducer。

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
import React, { useReducer } from 'react';

// 计数器reducer
function countReducer(state, action) {
switch (action.type) {
case 'inc':
return state + 1;
case 'dec':
return state - 1;
default:
return state;
}
}

// 显隐reducer
function toggleReducer(state) {
return !state;
}

export default function DoubleReducerDemo() {
const [count, dispatchCount] = useReducer(countReducer, 0);
const [show, toggleShow] = useReducer(toggleReducer, true);

return (
<div>
<button onClick={() => toggleShow()}>Toggle Counter</button>
{show && (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatchCount({ type: 'inc' })}>+1</button>
<button onClick={() => dispatchCount({ type: 'dec' })}>-1</button>
</div>
)}
</div>
);
}

那么当它插入第一个hook的时候,结构是这样的

如果workInProgressHook不是null,则说明这个Fiber节点上面已经有hooks了。workInProgressHook = workInProgressHook.next = hook;相当于workInProgressHook.next = hook;workInProgressHook = workInProgressHook.next;这样两句话,意思就是把刚创建好的hook插入到链表的尾部,并且更新链表的尾节点,便于下一次插入。

也就是说,fiber里面的hooks的结构是这样的:

最后,这个方法返回了刚才新创建好的这个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
35
36
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
if (shouldDoubleInvokeUserFnsInHooksDEV) {
setIsStrictModeForDevtools(true);
try {
init(initialArg);
} finally {
setIsStrictModeForDevtools(false);
}
}
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, A> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}

其实就比较贴合我们对useReducer的认知了,根据初始值和初始函数,算出一个initialState。然后把这个值,同时赋给hook.memoizedStatehook.baseState。接下来,再给hook.queuedispatch赋值,然后return [hook.memoizedState, dispatch];,就结束了。

我们先来看一下给queue赋值的地方,我依旧是用注释,说明一下各个字段的含义

1
2
3
4
5
6
7
const queue: UpdateQueue<S, A> = {
pending: null, // 存放update的数组,表示待处理的更新
lanes: NoLanes, // 这个hook的优先级
dispatch: null, //更新的方法
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
};

最下面两个字段的含义顾名思义,但是我看useReducer里面好像没看到哪里用到它,所以不清楚实际作用是什么,暂时略过吧。

然后我们看下面的dispatch方法,是给dispatchReducerAction方法绑死了currentlyRenderingFiber和queue参数。我们可以看看他的具体实现,略过开发环境的代码

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 dispatchReducerAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
const lane = requestUpdateLane(fiber);

const update: Update<S, A> = {
lane, //优先级
revertLane: NoLane, //revert它的优先级,和乐观更新有关
action, //具体的更新方法
hasEagerState: false, //是否可以直接复用上一次的值
eagerState: null, // 上一次的值(如果可复用,就无需再计算)
next: (null: any), // 下一个update
};

if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
startUpdateTimerByLane(lane);
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
}
}
}

这个方法其实就是reducer的第二个参数,常见的dispatch,我们调用它去改变状态。他所做的事情,就是创建一个update对象,然后把这个update推到queue里面。具体逻辑我们暂不深究,等后面讲到相关的部分再来细讲。

也就是说,hook里面的主要结构是这样的

updateReducer

当函数组件重新渲染的时候,再执行到这个hook的时候,就会执行updateReducer。我们看看updateReducer执行了什么。

1
2
3
4
5
6
7
8
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}

对比mountReducer,一开始从mountWorkInProgressHook,变成了updateWorkInProgressHookmountWorkInProgressHook是在组件mount的时候,把每一个hook都挂到fiber上面。而updateReducer,则是从current fiber上,把所有的hook都copy到workInProgress fiber上面。

为了节省篇幅,具体代码就不看了。代码也不复杂,抛去各种容错的代码,其实就是遍历一个老链表,把每个节点都加到新链表的末尾(每个hook节点都是新创建的字面量,但是这个节点的各个字段,例如memoizedState,和queue都和老节点值相等)

下面的updateReducerImpl是重头戏,它包含了useReducer最核心,也是最复杂的代码。里面为了处理可中断渲染,并发更新的逻辑,写得很复杂。我们可能关注核心逻辑,对于较为复杂的逻辑,我们简单讲解,但不花太多口舌。等到后面讲到React 18开始的新API的时候,涉及到并发渲染,我们再来详细讲吧。

对于大篇幅的代码,我们分段来看。

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
function updateReducerImpl<S, A>(
hook: Hook,
current: Hook,
reducer: (S, A) => S,
): [S, Dispatch<A>] {
const queue = hook.queue;
// The last rebase update that is NOT part of the base state.
let baseQueue = hook.baseQueue;

// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
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;
}
// 以下部分先省略
}

我们首先拿到了hook.queue.pending,记作pengingQueue,里面的内容是这一次新增的update。然后拿到了hook.baseQueue记作baseQueue,里面的内容是上一次渲染的时候因为优先级不够,而没有被执行的update。

简单提一下,React18开始,部分API可以使得update的优先级变低,从而使得主线程优先完成高优先级的渲染,之后有时间了再来完成低优先级的更新。所以低优先级的更新会被暂存在baseQueue里。

pendingQueuebaseQueue的结构,都是一个环形链表,并且这两个变量都指向环形链表的尾节点。也就是说,pendingQueue/baseQueue是环形链表的尾,pendingQueue.next/baseQueue.next是环形链表的头。

这里之所以用这样的数据结构来记录update,是因为这里是一个经常需要在尾部插入的场景。如果用普通的单链表,记录头节点的话,那么在尾部插入的时间复杂度是O(n)。而如果使用环形链表,并且记录尾节点,则在尾部插入的时间复杂度则是O(1)。同时,由于它是个环,那么从尾部找到它的头,并且从头开始遍历,也很简单,几乎和普通单链表没有差距。如下图

而上面那一大段代码,就是把baseQueue和pendingQueue做一个合并。把这次新增的,加到上一次遗留后面,并且保证遍历的时候,先遍历到上一次遗留的。这很合理,先来后到。下面的图演示了两个环形链表如何合并成一个新的环形链表。且永远用环形链表的尾指针来表示这个链表。

我们看下面这张图,来理解一下上面合并两个环形链表的代码

合并完毕后,将current.baseQueue,baseQueue,pendingQueue都指向pendingQueue,得到一个新的环形链表。这个环形链表的头节点,是原来的baseFirst。这样遍历这个链表的时候,就会先遍历到原来baseQueue的内容,然后再遍历到pendingQueue上面的内容,保证了先来后到。

我们继续看下面的代码。两个环形链表已经拼接完了,就该遍历链表,逐个取出update去更新了。这里代码实在太多,我不逐行讲解了,而是在关键位置加上注释。在后面只做简短的讲解。

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
function updateReducerImpl<S, A>(
hook: Hook,
current: Hook,
reducer: (S, A) => S,
): [S, Dispatch<A>] {
const baseState = hook.baseState;
if (baseQueue === null) {
hook.memoizedState = baseState;
} else {
// We have a queue to process.
const first = baseQueue.next;
let newState = baseState;

let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast: Update<S, A> | null = null;
let update = first;
let didReadFromEntangledAsyncAction = false;
// 开始遍历
do {
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;

// 看updateLane是不是左边lane的子集
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);

// update的优先级低,这次跳过
if (shouldSkipUpdate) {
// 拷贝一个当前update,存到新的newBaseQueue里面(遗留,作为下一次渲染的baseQueue)
const clone: Update<S, A> = {
lane: updateLane,
revertLane: update.revertLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
// newBaseQueue为空,则创建一个环形链表
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
// newBaseQueue不为空,则往环形链表尾部插入一个节点(这里只是插入尾部,不用每插一个就重新成 // 环。等循环结束了所有节点都插入完毕了,再让新尾部指回头部,组成环形结构)
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
// 本次要执行这个update
const revertLane = update.revertLane;
if (revertLane === NoLane) {
// 特别地,如果newBaseQueue已经不为空了,即前面已经有update被跳过,那么虽然当前update会被 // 执行,也依旧要把它拷贝一份,放到newBaseQueue尾部。这个原因会在后面讲解
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
lane: NoLane,
revertLane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}

if (updateLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}
} else {
// 这里也略吧,太长了。。
}

// 执行传进来的更新行为,计算出这个update后新的state
const action = update.action;
if (shouldDoubleInvokeUserFnsInHooksDEV) {
reducer(newState, action);
}
if (update.hasEagerState) {
newState = ((update.eagerState: any): S);
} else {
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);

if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
// newBaseQueue不为空,则让尾指向头,重新形成环形链表
newBaseQueueLast.next = (newBaseQueueFirst: any);
}

if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();

if (didReadFromEntangledAsyncAction) {
const entangledActionThenable = peekEntangledActionThenable();
if (entangledActionThenable !== null) {
throw entangledActionThenable;
}
}
}

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

queue.lastRenderedState = newState;
}

我们看他这里的逻辑,主要就是遍历baseQueue,逐个取出里面的update。每个update有它自己的lane(优先级),我们前面已经看过了。这里的lane是用二进制位来表示的一个数字。用二进制来表示,就是为了能直观的看出位运算的结果。判断update是否要在这次渲染执行,就是看update的lane是否属于这次renderLanes的子集(即updateLane & renderLanes === updateLane?)。

如果优先级不属于这次渲染,那么就放到newBaseQueue里。我们上一part看到的baseQueue,说了含义是上一次渲染遗留的update,其实就是这么来的。

然后有个比较难懂的逻辑,就是虽然这个update优先级是要在这次执行的,但是当newBaseQueue不为空的时候,也要把这个update加进去。这个就和React的并发渲染有关。React的更新是有优先级的,比如如果你普通的useReducer,那么它就是高优先级。如果你用startTransition包裹了useReducer,那么它就是低优先级。

如果你像下面这样写,点击一次按钮的时候,会有两次更新。只不过一次是低优先级,一次是高优先级。

1
2
3
4
5
6
7
onClick = ()=>{
dispatchNum(n=>n+2)
startTransition(()=>{
dispatchNum(3)
})
dispatchNum(n=>n+1)
}

假设num初始值是0。我们只想最终结果,num应该是4。因为startTransition对于最终结果是不应该产生影响的。如果你为了性能优化,而影响了结果,谁还敢用呢?
那么第一次渲染,是高优先级的渲染,会先执行一次+2,在执行一次+1,此时n是3。然后第二次渲染,终于要执行低优先级的了。可这个低优先级的更新,是将num设为3,那这样是不是不太对,凑不成4?

所以他的真正逻辑是这样的。第一次渲染,先执行+2。然后等遍历到低优先级update的时候,发现这个update优先级不够了,于是把他存入newBaseQueue。同时,把当前的结果记作newBaseState,作为基准。意思就是,在此之前,所有的update都没有被跳过,执行完他们的状态是newBaseState。等以后在遍历的时候,不用管前面的update了,从第一个被跳过的update开始遍历,把newBaseState当做初始值就可以了。接着遍历第三个update,执行它,并且将它也推入到newBaseQueue

那么第一遍高优先级渲染执行完,state是3(0+2+1),让用户临时看到了高优先级的结果。并且newBaseState是2,newBaseQueue里分别是“设为3”和“+1”两个更新。第二遍执行的时候,会以newBaseState作为基准,然后把newBaseQueue里面的所有更新都重新执行一遍。这里面会有更新被重复执行。最终第二遍渲染执行完之后,state就是最终结果4了(2=>3+1)

那么以上,就是useReducer的逻辑


通俗易懂的React原理(三):函数组件和hooks原理
https://miku03090831.github.io/2025/07/29/通俗易懂的React原理(三):函数组件和hooks原理/
作者
qh_meng
发布于
2025年7月29日
许可协议