title |
---|
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
的使用. 再结合前文状态与副作用的介绍, 我们知道使用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属性
.
在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
混淆.
在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
的区别.
狭义上讲, useState, useReducer
可以在function组件
添加内部的state
, 且useState
实际上是useReducer
的简易封装, 是一个最特殊(简单)的useReducer
. 所以将useState, useReducer
称为状态Hook
.
广义上讲, 只要能实现数据持久化且没有副作用
的Hook
, 均可以视为状态Hook
, 所以还包括useContext, useRef, useCallback, useMemo
等. 这类Hook
内部没有使用useState/useReduer
, 但是它们也能实现多次render
时, 保持其初始值不变(即数据持久化)且没有任何副作用
.
得益于双缓冲技术(double buffering), 在多次render
时, 以fiber
为载体, 保证复用同一个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
)都是由上述 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
之前, 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;
每个变量的解释, 可以对照源码中的英文注释, 其中最重要的有:
currentlyRenderingFiber
: 当前正在构造的 fiber, 等同于 workInProgresscurrentHook 与 workInProgressHook
: 分别指向current.memoizedState
和workInProgress.memoizedState
注: 有关current
和workInProgress
的区别, 请回顾双缓冲技术(double buffering)
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;
}
- 调用
function
前: 设置全局变量, 标记渲染优先级
和当前fiber
, 清除当前fiber
的遗留状态. - 调用
function
: 构造出Hooks
链表, 最后生成子级ReactElement
对象(children
). - 调用
function
后: 重置全局变量, 返回children
.- 为了保证不同的
function
节点在调用时renderWithHooks
互不影响, 所以退出时重置全局变量.
- 为了保证不同的
在function
中, 如果使用了Hook api
(如: useEffect
, useState
), 就会创建一个与之对应的Hook
对象, 接下来重点分析这个创建过程.
import React, { useState, useEffect } from 'react';
export default function App() {
// 1. useState
const [a, setA] = useState(1);
// 2. useEffect
useEffect(() => {
console.log(`effect 1 created`);
});
// 3. useState
const [b] = useState(2);
// 4. useEffect
useEffect(() => {
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) {
// 链表中首个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
两个指针同时向后移动.
- 由于
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
状态的持久化.