图解React图解React
  • 原理解析
  • 高频算法
  • 面试题
⌘ K
基本概念
宏观包结构
两大工作循环
高频对象
运行核心
启动过程
reconciler 运作流程
优先级管理
调度原理
fiber 树构造(基础准备)
fiber 树构造(初次创建)
fiber 树构造(对比更新)
fiber 树渲染
状态管理
状态与副作用
Hook 原理(概览)
Hook 原理(状态Hook)
Hook 原理(副作用Hook)
context 原理
交互
合成事件
Copyright © 2023 | Powered by dumi

Hook 原理(概览)

在前文状态与副作用中, 总结了class组件, function组件中通过api去改变fiber节点的状态和副作用. 其中对于function组件来讲, 其内部则需要依靠Hook来实现.

官方文档上专门用了一个版块来介绍Hook, 这里摘抄了几个比较关心的问题(其他FAQ请移步官网):

  1. 引入Hook的动机?

    • 在组件之间复用状态逻辑很难; 复杂组件变得难以理解; 难以理解的 class. 为了解决这些实际开发痛点, 引入了Hook.
  2. Hook 是什么? 什么时候会用 Hook?

    • Hook 是一个特殊的函数, 它可以让你“钩入” React 的特性. 如, useState 是允许你在 React 函数组件中添加 state 的 Hook.
    • 如果你在编写函数组件并意识到需要向其添加一些 state, 以前的做法是必须将其转化为 class. 现在你可以在现有的函数组件中使用 Hook.
  3. 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 原理(状态)章节中具体分析.):

  1. hook.memoizedState: 保持在内存中的局部状态.
  2. hook.baseState: hook.baseQueue中所有update对象合并之后的状态.
  3. hook.baseQueue: 存储update对象的环形链表, 只包括高于本次渲染优先级的update对象.
  4. hook.queue: 存储update对象的环形链表, 包括所有优先级的update对象.
  5. 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, 创建一个状态Hook
const [isOnline, setIsOnline] = useState(null);
// 2. 调用useEffect, 创建一个副作用Hook
useEffect(() => {
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).memoizedState
let 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;

每个变量的解释, 可以对照源码中的英文注释, 其中最重要的有:

  1. currentlyRenderingFiber: 当前正在构造的 fiber, 等同于 workInProgress
  2. currentHook 与 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和update
ReactCurrentDispatcher.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;
}
  1. 调用function前: 设置全局变量, 标记渲染优先级和当前fiber, 清除当前fiber的遗留状态.
  2. 调用function: 构造出Hooks链表, 最后生成子级ReactElement对象(children).
  3. 调用function后: 重置全局变量, 返回children.
    • 为了保证不同的function节点在调用时renderWithHooks互不影响, 所以退出时重置全局变量.

调用 function

Hooks 构造

在function中, 如果使用了Hook api(如: useEffect, useState), 就会创建一个与之对应的Hook对象, 接下来重点分析这个创建过程.

有如下 demo:
Edit hook-summary

在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) {
// 链表中首个hook
currentlyRenderingFiber.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两个指针同时向后移动.

  1. 由于renderWithHooks函数设置了workInProgress.memoizedState=null, 所以workInProgressHook初始值必然为null, 只能从currentHook克隆.
  2. 而从currentHook克隆而来的newHook.next=null, 进而导致workInProgressHook链表需要完全重建.

所以function执行完成之后, 有关Hook的内存结构如下:

可以看到:

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

总结

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