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

Hook 原理(状态 Hook)

首先回顾一下前文Hook 原理(概览), 其主要内容有:

  1. function类型的fiber节点, 它的处理函数是updateFunctionComponent, 其中再通过renderWithHooks调用function.
  2. 在function中, 通过Hook Api(如: useState, useEffect)创建Hook对象.
    • 状态Hook实现了状态持久化(等同于class组件维护fiber.memoizedState).
    • 副作用Hook则实现了维护fiber.flags,并提供副作用回调(类似于class组件的生命周期回调)
  3. 多个Hook对象构成一个链表结构, 并挂载到fiber.memoizedState之上.
  4. fiber树更新阶段, 把current.memoizedState链表上的所有Hook按照顺序克隆到workInProgress.memoizedState上, 实现数据的持久化.

在此基础之上, 本节将深入分析状态Hook的特性和实现原理.

创建 Hook

在fiber初次构造阶段, useState对应源码mountState, useReducer对应源码mountReducer

mountState:

function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 1. 创建hook
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
// 2. 初始化hook的属性
// 2.1 设置 hook.memoizedState/hook.baseState
// 2.2 设置 hook.queue
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
// queue.lastRenderedReducer是内置函数
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
// 2.3 设置 hook.dispatch
const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch =
(dispatchAction.bind(null, currentlyRenderingFiber, queue): any));
// 3. 返回[当前状态, dispatch函数]
return [hook.memoizedState, dispatch];
}

mountReducer:

function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>] {
// 1. 创建hook
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
// 2. 初始化hook的属性
// 2.1 设置 hook.memoizedState/hook.baseState
hook.memoizedState = hook.baseState = initialState;
// 2.2 设置 hook.queue
const queue = (hook.queue = {
pending: null,
dispatch: null,
// queue.lastRenderedReducer是由外传入
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
// 2.3 设置 hook.dispatch
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
// 3. 返回[当前状态, dispatch函数]
return [hook.memoizedState, dispatch];
}

mountState和mountReducer逻辑简单: 主要负责创建hook, 初始化hook的属性, 最后返回[当前状态, dispatch函数].

唯一的不同点是hook.queue.lastRenderedReducer:

  • mountState使用的是内置的basicStateReducer

    function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
    return typeof action === 'function' ? action(state) : action;
    }
  • mountReducer使用的是外部传入自定义reducer

可见mountState是mountReducer的一种特殊情况, 即useState也是useReducer的一种特殊情况, 也是最简单的情况.

useState可以转换成useReducer:

const [state, dispatch] = useState({ count: 0 });
// 等价于
const [state, dispatch] = useReducer(
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
},
{ count: 0 },
);
// 当需要更新state时, 有2种方式
dispatch({ count: 1 }); // 1.直接设置
dispatch((state) => ({ count: state.count + 1 })); // 2.通过回调函数设置

useReducer的官网示例:

const [state, dispatch] = useReducer(
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
},
{ count: 0 },
);
// 当需要更新state时, 只有1种方式
dispatch({ type: 'decrement' });

可见, useState就是对useReducer的基本封装, 内置了一个特殊的reducer(后文不再区分useState, useReducer, 都以useState为例).创建hook之后返回值[hook.memoizedState, dispatch]中的dispatch实际上会调用reducer函数.

状态初始化

在useState(initialState)函数内部, 设置hook.memoizedState = hook.baseState = initialState;, 初始状态被同时保存到了hook.baseState,hook.memoizedState中.

  1. hook.memoizedState: 当前状态
  2. hook.baseState: 基础状态, 作为合并hook.baseQueue的初始值(下文介绍).

最后返回[hook.memoizedState, dispatch], 所以在function中使用的是hook.memoizedState.

状态更新

有如下代码:Edit hook-status

初次渲染时count = 0, 这时hook对象的内存状态如下:

点击button, 通过dispatch函数进行更新, dispatch实际就是dispatchAction:

function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
// 1. 创建update对象
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber); // Legacy模式返回SyncLane
const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// 2. 将update对象添加到hook.queue.pending队列
const pending = queue.pending;
if (pending === null) {
// 首个update, 创建一个环形链表
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// 渲染时更新, 做好全局标记
didScheduleRenderPhaseUpdateDuringThisPass =
didScheduleRenderPhaseUpdate = true;
} else {
// ...省略性能优化部分, 下文介绍
// 3. 发起调度更新, 进入`reconciler 运作流程`中的输入阶段.
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}

逻辑十分清晰:

  1. 创建update对象, 其中update.lane代表优先级(可回顾fiber 树构造(基础准备)中的update优先级).
  2. 将update对象添加到hook.queue.pending环形链表.
    • 环形链表的特征: 为了方便添加新元素和快速拿到队首元素(都是O(1)), 所以pending指针指向了链表中最后一个元素.
    • 链表的使用方式可以参考React 算法之链表操作
  3. 发起调度更新: 调用scheduleUpdateOnFiber, 进入reconciler 运作流程中的输入阶段.

从调用scheduleUpdateOnFiber开始, 进入了react-reconciler包, 其中的所有逻辑可回顾reconciler 运作流程, 本节只讨论状态Hook相关逻辑.

注意: 本示例中虽然同时执行了 3 次 dispatch, 会请求 3 次调度, 由于调度中心的节流优化, 最后只会执行一次渲染

在fiber树构造(对比更新)过程中, 再次调用function, 这时useState对应的函数是updateState

function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}

实际调用updateReducer.

在执行updateReducer之前, hook相关的内存结构如下:

function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>] {
// 1. 获取workInProgressHook对象
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
let baseQueue = current.baseQueue;
// 2. 链表拼接: 将 hook.queue.pending 拼接到 current.baseQueue
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 3. 状态计算
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
// 3.1 优先级提取update
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 优先级不够: 加入到baseQueue中, 等待下一次render
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
// 优先级足够: 状态合并
if (newBaseQueueLast !== null) {
// 更新baseQueue
const clone: Update<S, A> = {
lane: NoLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
if (update.eagerReducer === reducer) {
// 性能优化: 如果存在 update.eagerReducer, 直接使用update.eagerState.避免重复调用reducer
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
// 调用reducer获取最新状态
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
// 3.2. 更新属性
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 把计算之后的结果更新到workInProgressHook上
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}

updateReducer函数, 代码相对较长, 但是逻辑分明:

  1. 调用updateWorkInProgressHook获取workInProgressHook对象

  2. 链表拼接: 将 hook.queue.pending 拼接到 current.baseQueue

  3. 状态计算

    1. update优先级不够: 加入到 baseQueue 中, 等待下一次 render

    2. update优先级足够: 状态合并

    3. 更新属性

性能优化

dispatchAction函数中, 在调用scheduleUpdateOnFiber之前, 针对update对象做了性能优化.

  1. queue.pending中只包含当前update时, 即当前update是queue.pending中的第一个update
  2. 直接调用queue.lastRenderedReducer,计算出update之后的 state, 记为eagerState
  3. 如果eagerState与currentState相同, 则直接退出, 不用发起调度更新.
  4. 已经被挂载到queue.pending上的update会在下一次render时再次合并.
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
// ...省略无关代码 ...只保留性能优化部分代码:
// 下面这个if判断, 能保证当前创建的update, 是`queue.pending`中第一个`update`. 为什么? 发起更新之后fiber.lanes会被改动(可以回顾`fiber 树构造(对比更新)`章节), 如果`fiber.lanes && alternate.lanes`没有被改动, 自然就是首个update
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
// 暂存`eagerReducer`和`eagerState`, 如果在render阶段reducer==update.eagerReducer, 则可以直接使用无需再次计算
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// 快速通道, eagerState与currentState相同, 无需调度更新
// 注: update已经被添加到了queue.pending, 并没有丢弃. 之后需要更新的时候, 此update还是会起作用
return;
}
}
}
// 发起调度更新, 进入`reconciler 运作流程`中的输入阶段.
scheduleUpdateOnFiber(fiber, lane, eventTime);
}

为了验证上述优化, 可以查看这个 demo:Edit hook-throttle

异步更新

上述示例都是为在Legacy模式下, 所以均为同步更新. 所以update对象会被全量合并,hook.baseQueue和hook.baseState并没有起到实质作用.

虽然在v17.x版本中, 并没有Concurrent模式的入口, 即将发布的v18.x版本将全面进入异步时代, 所以本节提前梳理一下update异步合并的逻辑. 同时加深hook.baseQueue和hook.baseState的理解.

假设有一个queue.pending链表, 其中update优先级不同, 绿色表示高优先级, 灰色表示低优先级, 红色表示最高优先级.

在执行updateReducer之前, hook.memoizedState有如下结构(其中update3, update4是低优先级):

链表拼接:

  • 和同步更新时一致, 直接把queue.pending拼接到current.baseQueue

状态计算:

  • 只会提取update1, update2这 2 个高优先级的update, 所以最后memoizedState=2
  • 保留其余低优先级的update, 等待下一次render
  • 从第一个低优先级update3开始, 随后的所有update都会被添加到baseQueue, 由于update2已经是高优先级, 会设置update2.lane=NoLane将优先级升级到最高(红色表示).
  • 而baseState代表第一个低优先级update3之前的state, 在本例中, baseState=1

function节点被处理完后, 高优先级的update, 会率先被使用(memoizedState=2). 一段时间后, 低优先级update3, update4符合渲染, 这种情况下再次执行updateReducer重复之前的步骤.

链表拼接:

  • 由于queue.pending = null, 故拼接前后没有实质变化

状态计算:

  • 现在所有update.lane都符合渲染优先级, 所以最后的内存结构与同步更新一致(memoizedState=4,baseState=4).

结论: 尽管update链表的优先级不同, 中间的render可能有多次, 但最终的更新结果等于update链表按顺序合并.

总结

本节深入分析状态Hook即useState的内部原理, 从同步,异步更新理解了update对象的合并方式, 最终结果存储在hook.memoizedState供给function使用.