Redux的作者Dan加入React核心团队后的一大贡献就是“将Redux的理念带入React”。

这里面最显而易见的影响莫过于useStateuseReducer这两个Hook。本质来说,useState只是预置了reduceruseReducer

本节我们来学习useStateuseReducer的实现。

流程概览

我们将这两个Hook的工作流程分为声明阶段调用阶段,对于:

  1. function App() {
  2. const [state, dispatch] = useReducer(reducer, {a: 1});
  3. const [num, updateNum] = useState(0);
  4. return (
  5. <div>
  6. <button onClick={() => dispatch({type: 'a'})}>{state.a}</button>
  7. <button onClick={() => updateNum(num => num + 1)}>{num}</button>
  8. </div>
  9. )
  10. }

声明阶段App调用时,会依次执行useReduceruseState方法。

调用阶段即点击按钮后,dispatchupdateNum被调用时。

声明阶段

FunctionComponent进入render阶段beginWork时,会调用renderWithHooksuseState与useReducer - 图1 (opens new window)方法。

该方法内部会执行FunctionComponent对应函数(即fiber.type)。

你可以在这里useState与useReducer - 图2 (opens new window)看到这段逻辑

对于这两个Hook,他们的源码如下:

  1. function useState(initialState) {
  2. var dispatcher = resolveDispatcher();
  3. return dispatcher.useState(initialState);
  4. }
  5. function useReducer(reducer, initialArg, init) {
  6. var dispatcher = resolveDispatcher();
  7. return dispatcher.useReducer(reducer, initialArg, init);
  8. }

正如上一节dispatcher所说,在不同场景下,同一个Hook会调用不同处理函数。

我们分别讲解mountupdate两个场景。

mount时

mount时,useReducer会调用mountReduceruseState与useReducer - 图3 (opens new window)useState会调用mountStateuseState与useReducer - 图4 (opens new window)

我们来简单对比这这两个方法:

  1. function mountState<S>(
  2. initialState: (() => S) | S,
  3. ): [S, Dispatch<BasicStateAction<S>>] {
  4. // 创建并返回当前的hook
  5. const hook = mountWorkInProgressHook();
  6. // ...赋值初始state
  7. // 创建queue
  8. const queue = (hook.queue = {
  9. pending: null,
  10. dispatch: null,
  11. lastRenderedReducer: basicStateReducer,
  12. lastRenderedState: (initialState: any),
  13. });
  14. // ...创建dispatch
  15. return [hook.memoizedState, dispatch];
  16. }
  17. function mountReducer<S, I, A>(
  18. reducer: (S, A) => S,
  19. initialArg: I,
  20. init?: I => S,
  21. ): [S, Dispatch<A>] {
  22. // 创建并返回当前的hook
  23. const hook = mountWorkInProgressHook();
  24. // ...赋值初始state
  25. // 创建queue
  26. const queue = (hook.queue = {
  27. pending: null,
  28. dispatch: null,
  29. lastRenderedReducer: reducer,
  30. lastRenderedState: (initialState: any),
  31. });
  32. // ...创建dispatch
  33. return [hook.memoizedState, dispatch];
  34. }

其中mountWorkInProgressHook方法会创建并返回对应hook,对应极简Hooks实现useState方法的isMount逻辑部分。

可以看到,mount时这两个Hook的唯一区别为queue参数的lastRenderedReducer字段。

queue的数据结构如下:

  1. const queue = (hook.queue = {
  2. // 与极简实现中的同名字段意义相同,保存update对象
  3. pending: null,
  4. // 保存dispatchAction.bind()的值
  5. dispatch: null,
  6. // 上一次render时使用的reducer
  7. lastRenderedReducer: reducer,
  8. // 上一次render时的state
  9. lastRenderedState: (initialState: any),
  10. });

其中,useReducerlastRenderedReducer为传入的reducer参数。useStatelastRenderedReducerbasicStateReducer

basicStateReducer方法如下:

  1. function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  2. return typeof action === 'function' ? action(state) : action;
  3. }

可见,useStatereducer参数为basicStateReduceruseReducer

mount时的整体运行逻辑与极简实现isMount逻辑类似,你可以对照着看。

update时

如果说mount时这两者还有区别,那update时,useReduceruseState调用的则是同一个函数updateReduceruseState与useReducer - 图5 (opens new window)

  1. function updateReducer<S, I, A>(
  2. reducer: (S, A) => S,
  3. initialArg: I,
  4. init?: I => S,
  5. ): [S, Dispatch<A>] {
  6. // 获取当前hook
  7. const hook = updateWorkInProgressHook();
  8. const queue = hook.queue;
  9. queue.lastRenderedReducer = reducer;
  10. // ...同update与updateQueue类似的更新逻辑
  11. const dispatch: Dispatch<A> = (queue.dispatch: any);
  12. return [hook.memoizedState, dispatch];
  13. }

整个流程可以概括为一句话:

找到对应的hook,根据update计算该hook的新state并返回。

mount时获取当前hook使用的是mountWorkInProgressHook,而update时使用的是updateWorkInProgressHook,这里的原因是:

  • mount时可以确定是调用ReactDOM.render或相关初始化API产生的更新,只会执行一次。

  • update可能是在事件回调或副作用中触发的更新或者是render阶段触发的更新,为了避免组件无限循环更新,后者需要区别对待。

举个render阶段触发的更新的例子:

  1. function App() {
  2. const [num, updateNum] = useState(0);
  3. updateNum(num + 1);
  4. return (
  5. <button onClick={() => updateNum(num => num + 1)}>{num}</button>
  6. )
  7. }

在这个例子中,App调用时,代表已经进入render阶段执行renderWithHooks

App内部,调用updateNum会触发一次更新。如果不对这种情况下触发的更新作出限制,那么这次更新会开启一次新的render阶段,最终会无限循环更新。

基于这个原因,React用一个标记变量didScheduleRenderPhaseUpdate判断是否是render阶段触发的更新。

updateWorkInProgressHook方法也会区分这两种情况来获取对应hook

获取对应hook,接下来会根据hook中保存的state计算新的state,这个步骤同Update一节一致。

调用阶段

调用阶段会执行dispatchActionuseState与useReducer - 图6 (opens new window),此时该FunctionComponent对应的fiber以及hook.queue已经通过调用bind方法预先作为参数传入。

  1. function dispatchAction(fiber, queue, action) {
  2. // ...创建update
  3. var update = {
  4. eventTime: eventTime,
  5. lane: lane,
  6. suspenseConfig: suspenseConfig,
  7. action: action,
  8. eagerReducer: null,
  9. eagerState: null,
  10. next: null
  11. };
  12. // ...将update加入queue.pending
  13. var alternate = fiber.alternate;
  14. if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
  15. // render阶段触发的更新
  16. didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  17. } else {
  18. if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
  19. // ...fiber的updateQueue为空,优化路径
  20. }
  21. scheduleUpdateOnFiber(fiber, lane, eventTime);
  22. }
  23. }

整个过程可以概括为:

创建update,将update加入queue.pending中,并开启调度。

这里值得注意的是if...else...逻辑,其中:

  1. if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1)

currentlyRenderingFiberworkInProgressworkInProgress存在代表当前处于render阶段

触发更新时通过bind预先保存的fiberworkInProgress全等,代表本次更新发生于FunctionComponent对应fiberrender阶段

所以这是一个render阶段触发的更新,需要标记变量didScheduleRenderPhaseUpdate,后续单独处理。

再来关注:

  1. if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes))

fiber.lanes保存fiber上存在的update优先级

fiber.lanes === NoLanes意味着fiber上不存在update

我们已经知道,通过update计算state发生在声明阶段,这是因为该hook上可能存在多个不同优先级update,最终state的值由多个update共同决定。

但是当fiber上不存在update,则调用阶段创建的update为该hook上第一个update,在声明阶段计算state时也只依赖于该update,完全不需要进入声明阶段再计算state

这样做的好处是:如果计算出的state与该hook之前保存的state一致,那么完全不需要开启一次调度。即使计算出的state与该hook之前保存的state不一致,在声明阶段也可以直接使用调用阶段已经计算出的state

你可以在这里useState与useReducer - 图7 (opens new window)看到这段提前计算state的逻辑

小Tip

我们通常认为,useReducer(reducer, initialState)的传参为初始化参数,在以后的调用中都不可变。

但是在updateReducer方法中,可以看到lastRenderedReducer在每次调用时都会重新赋值。

  1. function updateReducer(reducer, initialArg, init) {
  2. // ...
  3. queue.lastRenderedReducer = reducer;
  4. // ...

也就是说,reducer参数是随时可变的。

reducer可变Demo

每秒useReducer使用的reducer会改变一次

点击按钮后会随时间不同会出现+1-1的效果

关注公众号,后台回复582获得在线Demo地址