星际穿越

react hooks(比如useState)为什么需要保证调用顺序

2025-04-08

参考链接

面试回答

  • 调用链路:useState->mountState->mountWorkInProgressHook
    • mountWorkInProgressHook 函数源码中,可以看出
      • hook 对象之间以单向链表的形式相互串联
  • 而后续状态更新(二次渲染)时执行的 updateState->updateReducer->updateWorkInProgressHook 函数
    • updateState 调用了 updateReducer,所以更新时是把 state 当作/等同于 reducer 来处理的
    • updateWorkInProgressHook 函数源码中,可以看出
      • 会按顺序去遍历之前构建好的链表,取出对应的数据信息进行渲染
      • 注意这个过程就像从数组中依次取值一样,是完全按照顺序(或者说索引)来的
  • 为什么用by order的方式,其他方案的问题

以 useState 为例,分析 React-Hooks 的调用链路

首次渲染

在首次渲染的流程中,useState 触发的一系列操作最后会落到 mountState 里面去,所以我们重点需要关注的就是 mountState 做了什么事情

mounState 的主要工作是初始化 Hooks。在整段源码中,最需要关注的是 mountWorkInProgressHook 方法,它为我们道出了 Hooks 背后的数据结构组织形式。以下是 mountWorkInProgressHook 方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function mountWorkInProgressHook() {
// 注意,单个 hook 是以对象的形式存在的
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};

if (workInProgressHook === null) {
// 这行代码每个 React 版本不太一样,但做的都是同一件事:将 hook 作为链表的头节点处理
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// 若链表不为空,则将 hook 追加到链表尾部
workInProgressHook = workInProgressHook.next = hook;
}

// 返回当前的 hook
return workInProgressHook;

}

hook 相关的所有信息收敛在一个 hook 对象里,而 hook 对象之间以单向链表的形式相互串联。

更新(二次渲染)

首次渲染和更新渲染的区别,在于调用的是 mountState,还是 updateState。mountState 做了什么,你已经非常清楚了;而 updateState 之后的操作链路,虽然涉及的代码有很多,但其实做的事情很容易理解:按顺序去遍历之前构建好的链表,取出对应的数据信息进行渲染。

调用链路:updateState->updateReducer->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
//当前正在 update 的 fiber 上的 hook
function updateWorkInProgressHook(): Hook {
// This function is used both for updates and for re-renders triggered by a
// render phase update. It assumes there is either a current hook we can
// clone, or a work-in-progress hook from a previous render pass that we can
// use as a base. When we reach the end of the base list, we must switch to
// the dispatcher used for mounts.
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;

currentHook = nextCurrentHook;
nextCurrentHook = currentHook !== null ? currentHook.next : null;
} else {
// Clone from the current hook.
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;

const newHook: Hook = {
memoizedState: currentHook.memoizedState,

baseState: currentHook.baseState,
queue: currentHook.queue,
baseUpdate: currentHook.baseUpdate,

next: null,
};

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

更新(二次渲染)的时候会发生什么事情:updateState 会依次遍历链表、读取数据并渲染。注意这个过程就像从数组中依次取值一样,是完全按照顺序(或者说索引)来的。因此 React 不会看你命名的变量名是 career 还是别的什么,它只认你这一次 useState 调用,于是它难免会认为:喔,原来你想要的是第一个位置的 hook 啊。

总结

hooks 的渲染是通过“依次遍历”来定位每个 hooks 内容的。如果前后两次读到的链表在顺序上出现差异,那么渲染的结果自然是不可控的。

这个现象有点像我们构建了一个长度确定的数组,数组中的每个坑位都对应着一块确切的信息,后续每次从数组里取值的时候,只能够通过索引(也就是位置)来定位数据。也正因为如此,Hooks 的本质其实是链表。