React - state hook
在上一篇文章 React Fiber - updateQueue 中分析了 class 组件中 updateQueu 实现原理。如果某个函数式组件需要拥有自己的状态则可以使用 state hook。这篇文章会继续分析 state hook 的实现原理。
state hook 基本原理
state hook 的使用方式非常简单,使用 useState 可以返回状态及其对状态进行操作的方法,例如:
const [state, setState] = useState(1)
useState 可以传入一个初始值,在使用 setState 对 state 进行修改后,下一次 render 时会返回最新的 state。在一个函数组件中,可以使用多个 useState 来管理多个状态:
function MyComponent(props){
const [state1, setState1] = useState(1)
const [state2, setState2] = useState(2)
}
在有了多个 useState 的情况下,一个重要的问题就是调用某个 useState 后需要返回正确的 state。例如在这个例子中,第二次调用 useState 就需要返回 state2 而不是 state1。那么 react 是如何处理这种情况呢?
简单的来说,react 是通过链表来管理多个 useState 调时返回的 state。当每次调用 useState 时移动指针获取当前需要返回的 state。用代码简单的模拟一下:
interface Hook{
state: any, // 存储状态
next: Hook|null // 指向下一个 hook
}
let initHook: Hook = { // 第一个 hook,每次 render 时需要从该节点的 next 开始
state: null,
next: null
}
let previousHook: Hook|null = initHook;
function useState<T>(init: T): [T, (val: T) => void]{
if(previousHook.next === null){ // 当前 hook 不存在,使用初始化值
previousHook.next = {
state: init,
next: null
}
}
const hook = previousHook.next; // 获取当前 hook
function setState(state: T){
hook.state = state
}
previousHook = hook; // previousHook 置为当前 hook
return [hook.state, setState] // 返回 state 以及 setState
}
function MyComponent(){
const [state1, setState1] = useState(1)
const [state2, setState2] = useState(2)
console.info(state1)
console.info(state2)
setState1(4)
setState2(5)
}
MyComponent() // 1, 2 模拟 render 过程
previousHook = initHook
MyComponent() // 4, 5 模拟 render 过程
在上述代码中使用 Hook 来存储 state 以及下一个 hook。每次 useState 时移动链表指针来获取需要返回的 hook。这也能解释为什么官方文档中提到不能在 if 以及 for 语句中使用 hook,原因就是 state 返回是按顺序进行的,例如:
if(condition){
const [state1, setState1] = useState(1)
}
const [state2, setState2] = useState(2)
假设第一次 render 时 condition 为 true, 第二次 render 时 condition 为 false。那么在第二次 render 时第一个 useState 不会执行,state2 则会拿到属于 state1 的值,for 语句同理。
当然,在 react 的源码中 hook 的实现不会这么简单。回想一下,class 组件中的 state 会有一个 updateQueue 来存放改变的状态。那么同样对于 hook 来说也是相同的道理。在 react 中,一个 hook 的 state 并不是一个值而是一个 updateQueue,我们来看看代码中 hook 定义:
type Hook = {
memoizedState: any,
baseState: any, // 等价于 updateQueue 中的 baseState
baseQueue: Update<any, any> | null, // 等价于 updateQueue 中 firstBaseUpdate 以及 lastBaseUpdate 组成链表。这里是一个环形链表,所以可以只保存一个 update
queue: UpdateQueue<any, any> | null, // 等价于 updateQueue 中的 shareding
next: Hook | null, // 指向下一个 hook
};
type UpdateQueue<S, A> = {
pending: Update<S, A> | null, // 等价于 updateQueue 中的 shareding.pending
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};
可以看到 Hook 里面很多概念都与 updateQueue 一致,所以还没看过这篇文章的同学赶紧去看看React Fiber - updateQueue。例如 Hook 中存在两条链表,一条是多个 useState 构成的 hook 链表,一条是挂载在单个 hook 上的 state 链表。画个图会更好理解:
因此对于单个 hook 的 update 处理与 updateQueue 几乎一致:根据优先级判断是否某些 update 跳过,合并 baseUpdate 链表和 pending 链表,以及更新 baseUpdate 链表等。
state hook 源码分析
useReducer
在完全深入 state hook 的源码之前,不得不介绍另一个功能更强大,实用性更广的 hook: useReducer。如果你使用过 Redux,那么可以说 useReducer 与 Redux 功能完全相同。只不过一个用来管理局部状态,一个用来管理全部状态。简单的介绍一下,useReducer 里面包含的几个概念:
- state: 就是我们理解的 state
- action: 表示某种操作
- dispatcher: 用于分发 action
- reducer: 用于接收 dispatcher 分发的 action,并更新 state
概念很多,我们用看看 react 提供的例子:
const initialState = {count: 0};
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();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useReducer 接收 reducer 返回一个 state 以及 dispatcher。reducer 根据 action 来更新state。例如接收到的 action.type == 'increment' 则返回 state - 1。
而实际上在 react 中 useState 实际上是使用与 useReducer 相同的手段实现的,里面同样涉及到 action, reducer 等概念。要注意的是这里说的相同的手段并不是指 useState 是使用 useReducer 实现的。这也是为什么在接受 useState 之前要先介绍 useReducer 的原因。
当然,我们可以先用 useReduer 模拟一个 useState 试试。useReuder 中需要我们自己定义 action 以及 reducer。useState 返回两个参数: state以及 setState,而在使用时 setStata 的输入总一个新状态或者对状态的变动函数,因此可以得出 useReducer 中的 action = setState 输入的参数。同样 reducer 也只需要介绍这个参数做一个简单的判断返回即可,例如:
import "./styles.css";
import * as React from 'react';
function simpleReducer(state, action){
return typeof(action) === 'function' ? action(state): action
}
function useState(init){
return React.useReducer(simpleReducer, init)
}
export default function App() {
const [val, setVal] = useState(1)
return (
<div className="App" onClick={() => setVal(2)}>
<h1>{val}</h1>
</div>
);
}
可以看出代码中只是定义了一个 simpleReducer 就能将 useReducer 转换为 useState,逻辑非常的简单。
useState 源码分析
react 中 useState 源码如下:
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
代码实际上只有几行,首先通过 resolveDispatcher 方法获取一个 dispatcher 并调用 dispatcher.useState。可以看出真正起作用的是 dispatcher.useState。接着看 resolveDispatcher 方法:
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
也简单的不能再简单了,直接返回了 ReactCurrentDispatcher.current。那么这个 ReactCurrentDispatcher.current 是在哪里被赋值的呢。答案就是在: renderWithHook 中。
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
当 ReactCurrentDispatcher.current 为空时,使用的是 HooksDispatcherOnMount 作为 current 否则使用的则是 HooksDispatcherOnUpdate 作为 current。那么再看这两个对象中 useState 的定义:
const HooksDispatcherOnMount = {
useState: mountState,
...
}
const HooksDispatcherOnUpdate = {
useState: updateState,
...
}
从代码中可以看出实际上起作用的是 mountState 以及 updateState。这里可以理解为当首次调用 setState 时调用为 mountState,而后续调用 setState 则调用的 updateState。继续看 mountState 的代码:
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook(); // 获取当前的 hook
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
}); // 初始化该 hook 对应的 queue
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));// 初始化 dispatch 方法,也就是代码中使用的 setState
return [hook.memoizedState, dispatch];
}
这里可以看到一开始调用的为 mountWorkInProgressHook,这个方法的作用是获得当前需要的 hook 对象。
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 如果当前一个 hook 都不存在,则新建一个 hook 并挂载到 fiber 的 memoizedState
// memoizedState 代表整个 hook 链的第一个 hook
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
// 否则新建一个 hook 并添加到 hook 链的尾部
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
可以看出与之前提到的使用链表存储多个 hook 一致。在代码初始化 queue 时使用了 basicStateReducer 作为 reducer。没错,这里的 lastRenderedReducer 就是之前提到的 useReducer 中的 reducer。会在后续更新 state 时使用。
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer, // 这里的 reducer 只对 eager state 生效
lastRenderedState: (initialState: any),
}); // 初始化该 hook 对应的 queue
与之前通过 useReducer 实现 useState 时一致。在 renderWithHook 最后还返回的 dispatchAaction 就是代码中使用的 setState,其源码为:
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// Append the update to the end of the list.
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
这里又看到了熟悉的代码,就是生成一个 update 并添加到 queue.pending 形成环形链表。和 updateQueue 处理非常相似。整个 mountState 代码就到此结束。再继续看 updateState 的代码:
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
updateState 代码中调用了 updateReducer。这也是之前为什么说 useState 和 useReducer 使用相同的原理实现的原因。看到 updateReducer 传入了 basicStateReducer 作为 reducer。继续看 updateReducer 源码:
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 处理 hook 链表,获取当前需要计算 state 的 hook
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
// 等价于 updateQueue 中 firstBaseState 以及 lastBaseState 形成的链表
let baseQueue = current.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
// 如果 pending 链表以及 baseState 链表都不为空,则需要合并形成本次更新需要的
// state 链表
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
// We have a queue to process.
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;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
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 {
// This update does have sufficient priority.
if (newBaseQueueLast !== null) {
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) {
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
// 这里的 baseQueue 是环形链表,所以最后一个 state 要指向第一个 state
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
从源码中可以看出 updateReducer 的源码与 processUpdateQueue 逻辑非常相似,都是根据 pedingQueue 以及 baseQueue 来计算最新状态。只不过 processUpdateQueue 中使用 getStateFromUpdate 获取最新的状态,而这里使用 reducer 来获取最新的状态。根据代码得知,这里的 reducer 实际上是 basicStateReducer:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
这和我们之前通过 useReducer 实现 useState 的思路完全一致。
总结
可以看出 state hook 也并没有使用什么复杂的技术,多个 hook 通过链表连接,而同一个 hook 的多个 state 也通过链表连接,这就是为什么不能在 if 以及 for 语句中使用的原因。对于同一个 hook 的状态处理,实际上与 class 组件中 updateQueue 的状态处理几乎一致。如果我们在理解了 updateQueue 再来看 state hook 就会觉得非常简单。