useReducer 源码解析
在前面,我们讲了 useState
的实现,以及 hook
的调用流程,相信你已经非常熟悉了, hooks 这部分的内容的相似度很高,只要对整个流程清楚了,剩下的就是 API 如何封装的事情了
这部分介绍 useReducer
的实现,useReducer
和 useState
非常相似,在 update
时,都调用 updateReducer
Mount 时
在 mount 阶段,当执行到 useReducer
方法时,会调用 mountReducer
进行处理
const HooksDispatcherOnMount: Dispatcher = {
useReducer: mountReducer,
};
mountReducer
会创建 Hook 对象,得到初始状态,创建 queue,生成 dispatch
给用户使用
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 创建 hook 对象
const hook = mountWorkInProgressHook();
// 生成初始状态,是否有第三个参数
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
// 创建 queue
const queue: UpdateQueue<S, A> = {...};
hook.queue = queue;
// 生成 dispatch
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(...): any));
return [hook.memoizedState, dispatch];
}
完整的流程如下
- 通过
mountWorkInProgressHook
创建 hook 对象 - 根据用户传递的参数,进行 state 的初始化
- 把初始状态挂到
memoizedState
和baseState
上 - 创建 queue 链表,挂到 hook 上
- 生成
dispatch
并返回数组
在这里有一点需要提醒的是,useReducer
支持第三个参数,这个参数是一个 Function,用于惰性初始化 state,其实 useState
也有这个功能, 惰性初始化是 React 用来优化的一种手段,我们可以看一个例子
const initialState = Number(window.localStorage.getItem('count'))
const [count, setCount] = React.useState(initialState)
当函数组件更新 re-render
时,函数组件内所有代码都会重新执行一遍。此时 initialState
的初始值是一个相对开销较大的 IO 操作。每次函数组件 re-render
时,第一行代码都会被执行一次,引起不必要的性能损耗。
const initialState = () => Number(window.localStorage.getItem('count'))
const [count, setCount] = React.useState(initialState)
当 initialState
以函数形式传入时,它只会在函数组件初始化的时候执行一次,函数 re-render
时不会再被执行。这个函数即惰性初始化函数这个特性,可以在这种场景下规避不必要的性能问题。
这就是惰性初始化的意义所在,避免计算不必要的 state
生成 Dispatch 函数
还有一个很关键的点,就是 dispatch
函数的生成,看到 dispatchReducerAction
这个方法
function dispatchReducerAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
//将update,加入到queue.pending环状链表中
//多次调用dispatch,创建的update都会加入到这个queue.pending环状链表中;
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
}
- 首先会去获取本次更新的优先级
- 然后创建 update
- 判断是否是 re-render 引起的更新(和
dispatchSetState
一样)- 如果是那就执行
enqueueRenderPhaseUpdate
,会将 update 加到 queue.pending 中,具体可看上一节
- 如果是那就执行
- 如果不是 re-render 的更新,也会将
queue
加入到 queue.pending 中,返回当前的root
节点,然后调用scheduleUpdateOnFiber
开始更新调度
图来自网络
update 时,updateReducer
这部分和 useState
一致,可以看前面的
- 根据上次更新或加载后,存储的组件
fiber
的hook
对象,创建新的hook
对象; - 拿到
hook
的更新对象环状链表queue.pending
,循环环状链表,算出新的状态; - 判断新老状态,如果不一样就标记更新;
- 把新的状态存储到 hook 对象;
- 把新的状态和 dispatch 返回给用户;
图来自网络
useState 和 useReducer 的区别
看到这里你一定很疑惑,useReducer
和 useState
这么相似,为啥还要搞两个 API,这不自找麻烦咩
首先,一些明显的不同,在使用上也能感受到
useState
只需要传递一个状态即可,而useReducer
需要reducer
和状态,使用上来说,useState
对开发者更加友好- 调用
dispatch
时,useState
需要判断状态是否相等再判断需不需要更新,而useReducer
是一把梭,因为它的状态由 reducer 来控制,reducer 是一个纯函数
对于一些使用场景,React 官方会更加推荐使用 useReducer
,可以看官网
前面 useState 和 useReducer 都没有讲到一个很重要的东西,就是 batchUpdate
,当多次调用 dispatch
时,React 是怎么处理批量更新的?
这个其实依赖的是 React 的 Scheduler 调度器实现的,当 dispatch
触发后,并不会立刻的去更新,而是调用的 scheduleUpdateOnFiber
来调度更新,在这里面,会把 更新处理添加到一个微任务队列里,好像叫 scheduleMicrotask
(忘记了,不想找),这样可以调用多次 dispatch
,而只执行一次更新
总结
useState
和 useReducer
的执行过程