title: Hook 原理(概览)
Hook 原理(概览)
在前文状态与副作用中, 总结了class组件, function组件中通过api去改变fiber节点的状态和副作用. 其中对于function组件来讲, 其内部则需要依靠Hook来实现.
官方文档上专门用了一个版块来介绍Hook, 这里摘抄了几个比较关心的问题(其他FAQ请移步官网):
-
- 在组件之间复用状态逻辑很难; 复杂组件变得难以理解; 难以理解的 class. 为了解决这些实际开发痛点, 引入了
Hook.
- 在组件之间复用状态逻辑很难; 复杂组件变得难以理解; 难以理解的 class. 为了解决这些实际开发痛点, 引入了
-
Hook是一个特殊的函数, 它可以让你“钩入”React的特性. 如,useState是允许你在React函数组件中添加state的Hook.- 如果你在编写函数组件并意识到需要向其添加一些
state, 以前的做法是必须将其转化为class. 现在你可以在现有的函数组件中使用Hook.
-
- 不会. 在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别. 除此之外,可以认为
Hook的设计在某些方面更加高效:Hook避免了class需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本.- 符合语言习惯的代码在使用
Hook时不需要很深的组件树嵌套. 这个现象在使用高阶组件、render props、和context的代码库中非常普遍. 组件树小了,React的工作量也随之减少.
- 不会. 在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别. 除此之外,可以认为
所以Hook是React团队在大量实践后的产物, 更优雅的代替class, 且性能更高. 故从开发使用者的角度来讲, 应该拥抱Hook所带来的便利.
Hook 与 Fiber
通过官网文档的讲解, 能快速掌握Hook的使用. 再结合前文状态与副作用的介绍, 我们知道使用Hook最终也是为了控制fiber节点的状态和副作用. 从fiber视角, 状态和副作用相关的属性如下(这里不再解释单个属性的意义, 可以回顾状态与副作用):
export type Fiber = {|// 1. fiber节点自身状态相关pendingProps: any,memoizedProps: any,updateQueue: mixed,memoizedState: any,// 2. fiber节点副作用(Effect)相关flags: Flags,nextEffect: Fiber | null,firstEffect: Fiber | null,lastEffect: Fiber | null,|};
使用Hook的任意一个api, 最后都是为了控制上述这几个fiber属性.
Hook 数据结构
在ReactFiberHooks中, 定义了Hook的数据结构:
type Update<S, A> = {|lane: Lane,action: A,eagerReducer: ((S, A) => S) | null,eagerState: S | null,next: Update<S, A>,priority?: ReactPriorityLevel,|};type UpdateQueue<S, A> = {|pending: Update<S, A> | null,dispatch: (A => mixed) | null,lastRenderedReducer: ((S, A) => S) | null,lastRenderedState: S | null,|};export type Hook = {|memoizedState: any, // 当前状态baseState: any, // 基状态baseQueue: Update<any, any> | null, // 基队列queue: UpdateQueue<any, any> | null, // 更新队列next: Hook | null, // next指针|};
从定义来看, Hook对象共有 5 个属性(有关这些属性的应用, 将在Hook 原理(状态)章节中具体分析.):
hook.memoizedState: 保持在内存中的局部状态.hook.baseState:hook.baseQueue中所有update对象合并之后的状态.hook.baseQueue: 存储update对象的环形链表, 只包括高于本次渲染优先级的update对象.hook.queue: 存储update对象的环形链表, 包括所有优先级的update对象.hook.next:next指针, 指向链表中的下一个hook.
所以Hook是一个链表, 单个Hook拥有自己的状态hook.memoizedState和自己的更新队列hook.queue(有关 Hook 状态的分析, 在Hook原理(状态)章节中解读).

注意: 其中hook.queue与fiber.updateQueue虽然都是update环形链表, 尽管update对象的数据结构与处理方式都高度相似, 但是这 2 个队列中的update对象是完全独立的. hook.queue只作用于hook对象的状态维护, 切勿与fiber.updateQueue混淆.
Hook 分类
在v17.0.2中, 共定义了14 种 Hook
export type HookType =| 'useState'| 'useReducer'| 'useContext'| 'useRef'| 'useEffect'| 'useLayoutEffect'| 'useCallback'| 'useMemo'| 'useImperativeHandle'| 'useDebugValue'| 'useDeferredValue'| 'useTransition'| 'useMutableSource'| 'useOpaqueIdentifier';
官网上已经将其分为了 2 个类别, 状态Hook(State Hook), 和副作用Hook(Effect Hook).
这里我们可以结合前文状态与副作用, 从fiber的视角去理解状态Hook与副作用Hook的区别.
状态 Hook
狭义上讲, useState, useReducer可以在function组件添加内部的state, 且useState实际上是useReducer的简易封装, 是一个最特殊(简单)的useReducer. 所以将useState, useReducer称为状态Hook.
广义上讲, 只要能实现数据持久化且没有副作用的Hook, 均可以视为状态Hook, 所以还包括useContext, useRef, useCallback, useMemo等. 这类Hook内部没有使用useState/useReducer, 但是它们也能实现多次render时, 保持其初始值不变(即数据持久化)且没有任何副作用.
得益于双缓冲技术(double buffering), 在多次render时, 以fiber为载体, 保证复用同一个Hook对象, 进而实现数据持久化. 具体实现细节, 在Hook原理(状态)章节中讨论.
副作用 Hook
回到fiber视角, 状态Hook实现了状态持久化(等同于class组件维护fiber.memoizedState), 那么副作用Hook则会修改fiber.flags. (通过前文fiber树构造系列的解读, 我们知道在performUnitOfWork->completeWork阶段, 所有存在副作用的fiber节点, 都会被添加到父节点的副作用队列后, 最后在commitRoot阶段处理这些副作用节点.)
另外, 副作用Hook还提供了副作用回调(类似于class组件的生命周期回调), 比如:
// 使用useEffect时, 需要传入一个副作用回调函数.// 在fiber树构造完成之后, commitRoot阶段会处理这些副作用回调useEffect(() => {console.log('这是一个副作用回调函数');}, []);
在react内部, useEffect就是最标准的副作用Hook. 其他比如useLayoutEffect以及自定义Hook, 如果要实现副作用, 必须直接或间接的调用useEffect.
有关useEffect具体实现细节, 在Hook原理(副作用)章节中讨论.
组合 Hook
虽然官网并无组合Hook的说法, 但事实上大多数Hook(包括自定义Hook)都是由上述 2 种 Hook组合而成, 同时拥有这 2 种 Hook 的特性.
- 在
react内部有useDeferredValue, useTransition, useMutableSource, useOpaqueIdentifier等. - 平时开发中,
自定义Hook大部分都是组合 Hook.
比如官网上的自定义 Hook例子:
import { useState, useEffect } from 'react';function useFriendStatus(friendID) {// 1. 调用useState, 创建一个状态Hookconst [isOnline, setIsOnline] = useState(null);// 2. 调用useEffect, 创建一个副作用HookuseEffect(() => {function handleStatusChange(status) {setIsOnline(status.isOnline);}ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);return () => {ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);};});return isOnline;}
调用 function 前
在调用function之前, react内部还需要提前做一些准备工作.
处理函数
从fiber树构造的视角来看, 不同的fiber类型, 只需要调用不同的处理函数返回fiber子节点. 所以在performUnitOfWork->beginWork函数中, 调用了多种处理函数. 从调用方来讲, 无需关心处理函数的内部实现(比如updateFunctionComponent内部使用了Hook对象, updateClassComponent内部使用了class实例).
本节讨论Hook, 所以列出其中的updateFunctionComponent函数:
// 只保留FunctionComponent相关:function beginWork(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,): Fiber | null {const updateLanes = workInProgress.lanes;switch (workInProgress.tag) {case FunctionComponent: {const Component = workInProgress.type;const unresolvedProps = workInProgress.pendingProps;const resolvedProps =workInProgress.elementType === Component? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);return updateFunctionComponent(current,workInProgress,Component,resolvedProps,renderLanes,);}}}function updateFunctionComponent(current,workInProgress,Component,nextProps: any,renderLanes,) {// ...省略无关代码let context;let nextChildren;prepareToReadContext(workInProgress, renderLanes);// 进入Hooks相关逻辑, 最后返回下级ReactElement对象nextChildren = renderWithHooks(current,workInProgress,Component,nextProps,context,renderLanes,);// 进入reconcile函数, 生成下级fiber节点reconcileChildren(current, workInProgress, nextChildren, renderLanes);// 返回下级fiber节点return workInProgress.child;}
在updateFunctionComponent函数中调用了renderWithHooks(位于ReactFiberHooks) , 至此Fiber与Hook产生了关联.
全局变量
在分析renderWithHooks函数前, 有必要理解ReactFiberHooks头部定义的全局变量(源码中均有英文注释):
// 渲染优先级let renderLanes: Lanes = NoLanes;// 当前正在构造的fiber, 等同于 workInProgress, 为了和当前hook区分, 所以将其改名let currentlyRenderingFiber: Fiber = (null: any);// Hooks被存储在fiber.memoizedState 链表上let currentHook: Hook | null = null; // currentHook = fiber(current).memoizedStatelet workInProgressHook: Hook | null = null; // workInProgressHook = fiber(workInProgress).memoizedState// 在function的执行过程中, 是否再次发起了更新. 只有function被完全执行之后才会重置.// 当render异常时, 通过该变量可以决定是否清除render过程中的更新.let didScheduleRenderPhaseUpdate: boolean = false;// 在本次function的执行过程中, 是否再次发起了更新. 每一次调用function都会被重置let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;// 在本次function的执行过程中, 重新发起更新的最大次数const RE_RENDER_LIMIT = 25;
每个变量的解释, 可以对照源码中的英文注释, 其中最重要的有:
currentlyRenderingFiber: 当前正在构造的 fiber, 等同于 workInProgresscurrentHook 与 workInProgressHook: 分别指向current.memoizedState和workInProgress.memoizedState
注: 有关current和workInProgress的区别, 请回顾双缓冲技术(double buffering)
renderWithHooks 函数
renderWithHooks源码看似较长, 但是去除 dev 后保留主干, 逻辑十分清晰. 以调用function为分界点, 逻辑被分为 3 个部分:
// ...省略无关代码export function renderWithHooks<Props, SecondArg>(current: Fiber | null,workInProgress: Fiber,Component: (p: Props, arg: SecondArg) => any,props: Props,secondArg: SecondArg,nextRenderLanes: Lanes,): any {// --------------- 1. 设置全局变量 -------------------renderLanes = nextRenderLanes; // 当前渲染优先级currentlyRenderingFiber = workInProgress; // 当前fiber节点, 也就是function组件对应的fiber节点// 清除当前fiber的遗留状态workInProgress.memoizedState = null;workInProgress.updateQueue = null;workInProgress.lanes = NoLanes;// --------------- 2. 调用function,生成子级ReactElement对象 -------------------// 指定dispatcher, 区分mount和updateReactCurrentDispatcher.current =current === null || current.memoizedState === null? HooksDispatcherOnMount: HooksDispatcherOnUpdate;// 执行function函数, 其中进行分析Hooks的使用let children = Component(props, secondArg);// --------------- 3. 重置全局变量,并返回 -------------------// 执行function之后, 还原被修改的全局变量, 不影响下一次调用renderLanes = NoLanes;currentlyRenderingFiber = (null: any);currentHook = null;workInProgressHook = null;didScheduleRenderPhaseUpdate = false;return children;}
- 调用
function前: 设置全局变量, 标记渲染优先级和当前fiber, 清除当前fiber的遗留状态. - 调用
function: 构造出Hooks链表, 最后生成子级ReactElement对象(children). - 调用
function后: 重置全局变量, 返回children.- 为了保证不同的
function节点在调用时renderWithHooks互不影响, 所以退出时重置全局变量.
- 为了保证不同的
调用 function
Hooks 构造
在function中, 如果使用了Hook api(如: useEffect, useState), 就会创建一个与之对应的Hook对象, 接下来重点分析这个创建过程.
import React, { useState, useEffect } from 'react';export default function App() {// 1. useStateconst [a, setA] = useState(1);// 2. useEffectuseEffect(() => {console.log(`effect 1 created`);});// 3. useStateconst [b] = useState(2);// 4. useEffectuseEffect(() => {console.log(`effect 2 created`);});return (<><button onClick={() => setA(a + 1)}>{a}</button><button>{b}</button></>);}
在function组件中, 同时使用了状态Hook和副作用Hook.
初次渲染时, 逻辑执行到performUnitOfWork->beginWork->updateFunctionComponent->renderWithHooks前, 内存结构如下(本节重点是Hook, 有关fiber树构造过程可回顾前文):

当执行renderWithHooks时, 开始调用function. 本例中, 在function内部, 共使用了 4 次Hook api, 依次调用useState, useEffect, useState, useEffect.
而useState, useEffect在fiber初次构造时分别对应mountState和mountEffect->mountEffectImpl
function mountState<S>(initialState: (() => S) | S,): [S, Dispatch<BasicStateAction<S>>] {const hook = mountWorkInProgressHook();// ...省略部分本节不讨论return [hook.memoizedState, dispatch];}function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {const hook = mountWorkInProgressHook();// ...省略部分本节不讨论}
无论useState, useEffect, 内部都通过mountWorkInProgressHook创建一个 hook.
链表存储
而mountWorkInProgressHook非常简单:
function mountWorkInProgressHook(): Hook {const hook: Hook = {memoizedState: null,baseState: null,baseQueue: null,queue: null,next: null,};if (workInProgressHook === null) {// 链表中首个hookcurrentlyRenderingFiber.memoizedState = workInProgressHook = hook;} else {// 将hook添加到链表末尾workInProgressHook = workInProgressHook.next = hook;}return workInProgressHook;}
逻辑是创建Hook并挂载到fiber.memoizedState上, 多个Hook以链表结构保存.
本示例中, function调用之后则会创建 4 个hook, 这时的内存结构如下:

可以看到: 无论状态Hook或副作用Hook都按照调用顺序存储在fiber.memoizedState链表中.

顺序克隆
fiber树构造(对比更新)阶段, 执行updateFunctionComponent->renderWithHooks时再次调用function, 调用function前的内存结构如下:

注意: 在renderWithHooks函数中已经设置了workInProgress.memoizedState = null, 等待调用function时重新设置.
接下来调用function, 同样依次调用useState, useEffect, useState, useEffect. 而useState, useEffect在fiber对比更新时分别对应updateState->updateReducer和updateEffect->updateEffectImpl
// ----- 状态Hook --------function updateReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: I => S,): [S, Dispatch<A>] {const hook = updateWorkInProgressHook();// ...省略部分本节不讨论}// ----- 副作用Hook --------function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {const hook = updateWorkInProgressHook();// ...省略部分本节不讨论}
无论useState, useEffect, 内部调用updateWorkInProgressHook获取一个 hook.
function updateWorkInProgressHook(): Hook {// 1. 移动currentHook指针let nextCurrentHook: null | Hook;if (currentHook === null) {const current = currentlyRenderingFiber.alternate;if (current !== null) {nextCurrentHook = current.memoizedState;} else {nextCurrentHook = null;}} else {nextCurrentHook = currentHook.next;}// 2. 移动workInProgressHook指针let nextWorkInProgressHook: null | Hook;if (workInProgressHook === null) {nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;} else {nextWorkInProgressHook = workInProgressHook.next;}if (nextWorkInProgressHook !== null) {// 渲染时更新: 本节不讨论} else {currentHook = nextCurrentHook;// 3. 克隆currentHook作为新的workInProgressHook.// 随后逻辑与mountWorkInProgressHook一致const newHook: Hook = {memoizedState: currentHook.memoizedState,baseState: currentHook.baseState,baseQueue: currentHook.baseQueue,queue: currentHook.queue,next: null, // 注意next指针是null};if (workInProgressHook === null) {currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;} else {workInProgressHook = workInProgressHook.next = newHook;}}return workInProgressHook;}
updateWorkInProgressHook函数逻辑简单: 目的是为了让currentHook和workInProgressHook两个指针同时向后移动.
- 由于
renderWithHooks函数设置了workInProgress.memoizedState=null, 所以workInProgressHook初始值必然为null, 只能从currentHook克隆. - 而从
currentHook克隆而来的newHook.next=null, 进而导致workInProgressHook链表需要完全重建.
所以function执行完成之后, 有关Hook的内存结构如下:

可以看到:
- 以双缓冲技术为基础, 将
current.memoizedState按照顺序克隆到了workInProgress.memoizedState中. Hook经过了一次克隆, 内部的属性(hook.memoizedState等)都没有变动, 所以其状态并不会丢失.

总结
本节首先引入了官方文档上对于Hook的解释, 了解Hook的由来, 以及Hook相较于class的优势. 然后从fiber视角分析了fiber与hook的内在关系, 通过renderWithHooks函数, 把Hook链表挂载到了fiber.memoizedState之上. 利用fiber树内部的双缓冲技术, 实现了Hook从current到workInProgress转移, 进而实现了Hook状态的持久化.
