title |
---|
Hook 原理(状态Hook) |
首先回顾一下前文Hook 原理(概览), 其主要内容有:
function
类型的fiber
节点, 它的处理函数是updateFunctionComponent, 其中再通过renderWithHooks调用function
.- 在
function
中, 通过Hook Api
(如:useState, useEffect
)创建Hook
对象.状态Hook
实现了状态持久化(等同于class组件
维护fiber.memoizedState
).副作用Hook
则实现了维护fiber.flags
,并提供副作用回调
(类似于class组件
的生命周期回调)
- 多个
Hook
对象构成一个链表结构
, 并挂载到fiber.memoizedState
之上. fiber树
更新阶段, 把current.memoizedState
链表上的所有Hook
按照顺序克隆到workInProgress.memoizedState
上, 实现数据的持久化.
在此基础之上, 本节将深入分析状态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
使用的是内置的basicStateReducerfunction 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.通过回调函数设置
userReducer
的官网示例:
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]
中的dispath
实际上会调用reducer
函数.
在useState(initialState)
函数内部, 设置hook.memoizedState = hook.baseState = initialState;
, 初始状态被同时保存到了hook.baseState
,hook.memoizedState
中.
hook.memoizedState
: 当前状态hook.baseState
:基础
状态, 作为合并hook.baseQueue
的初始值(下文介绍).
最后返回[hook.memoizedState, dispatch]
, 所以在function
中使用的是hook.memoizedState
.
import { useState } from 'react';
export default function App() {
const [count, dispatch] = useState(0);
return (
<button
onClick={() => {
dispatch(1);
dispatch(3);
dispatch(2);
}}
>
{count}
</button>
);
}
初次渲染时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);
}
}
逻辑十分清晰:
- 创建
update
对象, 其中update.lane
代表优先级(可回顾fiber 树构造(基础准备)中的update优先级
). - 将
update
对象添加到hook.queue.pending
环形链表.环形链表
的特征: 为了方便添加新元素和快速拿到队首元素(都是O(1)
), 所以pending
指针指向了链表中最后一个元素.- 链表的使用方式可以参考React 算法之链表操作
- 发起调度更新: 调用
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
函数, 代码相对较长, 但是逻辑分明:
-
调用
updateWorkInProgressHook
获取workInProgressHook
对象 -
链表拼接: 将
hook.queue.pending
拼接到current.baseQueue
-
状态计算
dispatchAction
函数中, 在调用scheduleUpdateOnFiber
之前, 针对update
对象做了性能优化.
queue.pending
中只包含当前update
时, 即当前update
是queue.pending
中的第一个update
- 直接调用
queue.lastRenderedReducer
,计算出update
之后的 state, 记为eagerState
- 如果
eagerState
与currentState
相同, 则直接退出, 不用发起调度更新. - 已经被挂载到
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);
}
上述示例都是为在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
使用.