在前面的章节,我们学习了在函数组件内通过自定义构建的状态来执行操作。在这一章,在本章中,我们将深入探讨在构建优质状态解决方案时所面临的挑战,随后将了解 React 是如何借助底层的 Hook 来构建这一解决方案的。之后,我们要引入钩子的概念,理解其调用顺序的重要性,并理解为什么要避免条件性钩子函数。在本章的附录部分,会讨论两个额外主题: React Fiber 与 Current , 和 WorkInProgress 场景。
本章会讲解这些主题
- 创造一个好的状态方案
- 介绍一个React钩子
- 什么是钩子?
- 答疑环节
- 附录
创造一个好的状态方案
状态(States)太他妈重要了。一个没有状态(states)组件,就好比一个 没有了变量的函数。这样的组件会失去推理、运算的能力。一个UI组件正是因为有了状态(states),才能够与用户的行为产生交互。
在上一章中,我们构建了一个自定义状态(方案),代码如下:
js
let states = {}
function _getM2(initialValue, key) {
if (states[key] === undefined) {
states[key] = initialValue
}
return states[key]
}
function _setM2(v, key) {
states[key] = v
ReactDOM.render(<Title />, rootEl)
}
虽然这个方案可以跑起来,但是有几个问题我们要先抛出来。
首先,这个 states 对象 要放在哪里是一个 重要的问题:
js
let states = {}
在先前的代码中,states 对象是作为一个全局对象而存在的,但是我们通常更关心的是状态(states) 如何 与 组件 发生关联。换言之,我们需要发现一个地方来定义 本地状态(states)。
第二个问题是,我们如何得到每一个状态的唯一索引:
js
const a = _getM2(0, 'comp_a');
正如在之前的状态使用方式中那样,当我们将状态命名为 comp_a
后,只要是涉及该状态的任何操作,都必须携带这个键(comp_a
)。在一个典型的应用中,我们可能会有大量此类状态;如果每个状态都必须用一个唯一的字符串来定义,我们就得想出很多不重复的名称。要记录所有已使用过的名称会是相当繁琐的工作,更不用说函数组件内部存储该状态的变量本身已经有了一个名称(比如这里的 a
)。同时存在变量名和键字符串,不免有些累赘。
除了这两个 大问题外,还有一个小问题需要关注。在刚刚的事例中,每当我们更新状态时,这个Title
组件就要渲染。
对于开发者而言,知道每一次操作会更新哪些组件,是有一定难度的。如果React 引擎能自动帮我们识别要更新的组件,这是最好的。而这,正是React 做的最好的;我们应该借助引擎的这一能力来写代码,以更好地更新组件。最后但同样重要的是,我们知道状态(states)可用于实现不同的功能 ------ 其底层核心逻辑本就是一种 "数据持久化机制"。如果设计得当,我们完全可以基于这一机制搭建某种基础架构,进而在其上扩展更多附加功能。
介绍一个React 钩子
状态对于一个组件来说,是本地的。把状态放在组件的实例上,是符合直觉的,因为一个 React 组件 是 一个 UI的定义。那么,对于一个函数组件来说,其实例应该被存储在哪里。
实际上,组件并不是React的 最小单元。有一个更具颗粒度的架构------fiber。fiber 是用来代表一个元素(element)的。一个 fiber 会执行 与之对应的 元素(element)的所有任务。这个 元素 可以只是简单的原生 h1,div 元素,也可以是自定义组件。比如说,"fragment"元素可以下含一系列子元素,而不展示 "fragment"自身;一个memo
元素,可以存储上一次更新 该 element的结果。
事实上,一个函数组件正是一个 fiber 所代表的自定义元素。一个 函数组件做的,不过是允许我们 定义什么元素可以被展示,所以,无论何时这个 函数被调用,它可以知道哪些 DOM 元素需要展示在 屏幕上。更多的细节,你可以在附录 A ------ React Fiber部分了解到。
现在,我们知道了一个组件实例所对应的单元:而这个单元,正是React 存储状态的地方。React 会把这些状态 存储在 名为 memoizedState
的属性上, 而这个 属性又具有 Hook 结构:

我们在这里引入的 Hook
是一个 用于 存储 状态的 结构(你要说是 class 也行)。这个 Hook
并不是我们后面要介绍的 React hook(函数)。但麻烦的是,React在这两个地方 用了同一个名字。为了方便区分,我们会用 Hook 来代表一个 数据结构,而hook 来代表 函数。
ts
interface Hook {
memoizedState: any;
updateQueue: unknown;
baseState: any;
baseQueue: Update<any> | null;
next: Hook | null;
}
Hook
结构的主要功能,是通过 state
属性来存储当个状态。React不再是将多个状态存放在一个数组(或对象)中,而是通过链表(linked list)将这些状态相互关联起来,如下图。一个 Hook
对象 通过 next
指向另一个 Hook
对象。当到最后一个Hook
对象时,其 next
指向 为 null。

要让(React)引擎知晓界面是否存在变化,就需要对 Fiber 节点进行更新。而在 update,正是 Hooks 初始化的地方。接下来,我们看看更新函数。
更新一个函数组件
React 是 通过 updateFunctionComponent
函数 来更新 函数组件的。这个函数有两个参数 Component
,props
。
js
let updatingFiber = ...
function updateFunctionComponent(Component, props) {
prevHook = null
let children = Component(props)
}
这个更新函数的 主要作用是 调用Component(props)
来获得 children
元素。以 Title
组件为例,当它被更新时,会 调用 函数 Title()
。得到了 children
元素后,React 引擎会把这个元素 和 屏幕上的元素进行对比,并渲染 需要更新的部分。
在刚刚的更新函数中,有两个全局变量。它们是 updatingFiber
和 prevHook
。updatingFiber
代表的是React 引擎正在处理的 当前 fiber,而 prevHook
代表的是 这个 fiber 之前在 使用的 Hook 对象。在这个组件被调用前, updatingFiber
已经被引擎生成了。
组件首次更新时(比如在挂载阶段),就是该 Fiber 节点的第一个 Hook 被创建的时刻。
在挂载时创建一个 Hook 对象
为了在当前fiber 上 生成一个 Hook对象,React 会 生成 一个 新的 Hook 对象,并把它 插入到 链表里:
js
function mountHook() {
const Hook = {
state: null,
next: null
}
if (prevHook === null) {
updatingFiber.memoizedState = Hook;
prevHook = Hook
} else {
prevHook.next = Hook;
prevHook = prevHook.next;
}
return Hook;
}
在前面提到的 mountHook
函数中,首先会分配一个空的 Hook 对象,其 state
和 next
属性都被设为 null
。如果这是该 Fiber 节点的第一个 Hook(此时 preHook
为 null
),它会被存储在 Fiber 的 memoizedState
属性下;否则,它会被附加到前一个 Hook 的 next
属性上。之后,这个被分配的 Hook 会被返回。
在更新时获取一个Hook对象
当挂载完成后,我们可以访问React 在挂载时所生成的 Hook对象们。
js
function updateHook() {
var Hook;
if (prevHook === null) {
Hook = updatingFiber.memoizedState;
} else {
Hook = prevHook.next;
}
prevHook = Hook;
return Hook;
}
在当前的 updateHook
函数里,Hook 对象 是通过 查找updatingFiber
的 memoizedState
的 next
属性来实现的。而 prevHook
也会实时更新。这就是 Hook
的获取方式。
使用一个钩子
现在,我们已经构建了一个 与所有更新 同步的 钩子,我们可以在一个 函数组件里调用它,就像先前调用_getM
和 _getM2
那样。
现在,我们来实现一个 可以 接收 初始值的 _useHook
函数:
js
function _useHook(initialState) {
let Hook;
if (isFiberMounting) {
Hook = mountHook();
Hook.state = initialState
} else {
Hook = updateHook()
}
return Hook.state
}
通过 isFiberMounting
标志判断组件是否处于挂载阶段后,前面提到的 _useHook
函数会获取一个持久化的 Hook。若处于挂载阶段,React 会将 initialState
(初始状态)赋值给该 Hook;对于其他任何更新阶段,该 Hook 不会被修改。在所有情况下,最终都会返回该 Hook 中存储的状态。
你可能会好奇 React 是如何确定 isFiberMounting
标志的 ------ 由于这部分逻辑与引擎(React 核心机制)的关联更为深入,我们将相关内容放在了本章末尾的附录 B《Current 与 WorkInProgress 场景》中。
到目前为止,我们已经梳理了 React 引擎底层是如何实现 Hook 的。我们刚刚攻克了最复杂的部分,现在就让我们来看看如何使用它。