Redux的作者Dan加入React核心团队后的一大贡献就是“将Redux的理念带入React”。
这里面最显而易见的影响莫过于useState与useReducer这两个Hook。本质来说,useState只是预置了reducer的useReducer。
本节我们来学习useState与useReducer的实现。
流程概览
我们将这两个Hook的工作流程分为声明阶段和调用阶段,对于:
function App() {const [state, dispatch] = useReducer(reducer, {a: 1});const [num, updateNum] = useState(0);return (<div><button onClick={() => dispatch({type: 'a'})}>{state.a}</button><button onClick={() => updateNum(num => num + 1)}>{num}</button></div>)}
声明阶段即App调用时,会依次执行useReducer与useState方法。
调用阶段即点击按钮后,dispatch或updateNum被调用时。
声明阶段
当FunctionComponent进入render阶段的beginWork时,会调用renderWithHooks
(opens new window)方法。
该方法内部会执行FunctionComponent对应函数(即fiber.type)。
你可以在这里
(opens new window)看到这段逻辑
对于这两个Hook,他们的源码如下:
function useState(initialState) {var dispatcher = resolveDispatcher();return dispatcher.useState(initialState);}function useReducer(reducer, initialArg, init) {var dispatcher = resolveDispatcher();return dispatcher.useReducer(reducer, initialArg, init);}
正如上一节dispatcher所说,在不同场景下,同一个Hook会调用不同处理函数。
我们分别讲解mount与update两个场景。
mount时
mount时,useReducer会调用mountReducer
(opens new window),useState会调用mountState
(opens new window)。
我们来简单对比这这两个方法:
function mountState<S>(initialState: (() => S) | S,): [S, Dispatch<BasicStateAction<S>>] {// 创建并返回当前的hookconst hook = mountWorkInProgressHook();// ...赋值初始state// 创建queueconst queue = (hook.queue = {pending: null,dispatch: null,lastRenderedReducer: basicStateReducer,lastRenderedState: (initialState: any),});// ...创建dispatchreturn [hook.memoizedState, dispatch];}function mountReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: I => S,): [S, Dispatch<A>] {// 创建并返回当前的hookconst hook = mountWorkInProgressHook();// ...赋值初始state// 创建queueconst queue = (hook.queue = {pending: null,dispatch: null,lastRenderedReducer: reducer,lastRenderedState: (initialState: any),});// ...创建dispatchreturn [hook.memoizedState, dispatch];}
其中mountWorkInProgressHook方法会创建并返回对应hook,对应极简Hooks实现中useState方法的isMount逻辑部分。
可以看到,mount时这两个Hook的唯一区别为queue参数的lastRenderedReducer字段。
queue的数据结构如下:
const queue = (hook.queue = {// 与极简实现中的同名字段意义相同,保存update对象pending: null,// 保存dispatchAction.bind()的值dispatch: null,// 上一次render时使用的reducerlastRenderedReducer: reducer,// 上一次render时的statelastRenderedState: (initialState: any),});
其中,useReducer的lastRenderedReducer为传入的reducer参数。useState的lastRenderedReducer为basicStateReducer。
basicStateReducer方法如下:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {return typeof action === 'function' ? action(state) : action;}
可见,useState即reducer参数为basicStateReducer的useReducer。
mount时的整体运行逻辑与极简实现的isMount逻辑类似,你可以对照着看。
update时
如果说mount时这两者还有区别,那update时,useReducer与useState调用的则是同一个函数updateReducer
(opens new window)。
function updateReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: I => S,): [S, Dispatch<A>] {// 获取当前hookconst hook = updateWorkInProgressHook();const queue = hook.queue;queue.lastRenderedReducer = reducer;// ...同update与updateQueue类似的更新逻辑const dispatch: Dispatch<A> = (queue.dispatch: any);return [hook.memoizedState, dispatch];}
整个流程可以概括为一句话:
找到对应的
hook,根据update计算该hook的新state并返回。
mount时获取当前hook使用的是mountWorkInProgressHook,而update时使用的是updateWorkInProgressHook,这里的原因是:
mount时可以确定是调用ReactDOM.render或相关初始化API产生的更新,只会执行一次。update可能是在事件回调或副作用中触发的更新或者是render阶段触发的更新,为了避免组件无限循环更新,后者需要区别对待。
举个render阶段触发的更新的例子:
function App() {const [num, updateNum] = useState(0);updateNum(num + 1);return (<button onClick={() => updateNum(num => num + 1)}>{num}</button>)}
在这个例子中,App调用时,代表已经进入render阶段执行renderWithHooks。
在App内部,调用updateNum会触发一次更新。如果不对这种情况下触发的更新作出限制,那么这次更新会开启一次新的render阶段,最终会无限循环更新。
基于这个原因,React用一个标记变量didScheduleRenderPhaseUpdate判断是否是render阶段触发的更新。
updateWorkInProgressHook方法也会区分这两种情况来获取对应hook。
获取对应hook,接下来会根据hook中保存的state计算新的state,这个步骤同Update一节一致。
调用阶段
调用阶段会执行dispatchAction
(opens new window),此时该FunctionComponent对应的fiber以及hook.queue已经通过调用bind方法预先作为参数传入。
function dispatchAction(fiber, queue, action) {// ...创建updatevar update = {eventTime: eventTime,lane: lane,suspenseConfig: suspenseConfig,action: action,eagerReducer: null,eagerState: null,next: null};// ...将update加入queue.pendingvar alternate = fiber.alternate;if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {// render阶段触发的更新didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;} else {if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {// ...fiber的updateQueue为空,优化路径}scheduleUpdateOnFiber(fiber, lane, eventTime);}}
整个过程可以概括为:
创建
update,将update加入queue.pending中,并开启调度。
这里值得注意的是if...else...逻辑,其中:
if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1)
currentlyRenderingFiber即workInProgress,workInProgress存在代表当前处于render阶段。
触发更新时通过bind预先保存的fiber与workInProgress全等,代表本次更新发生于FunctionComponent对应fiber的render阶段。
所以这是一个render阶段触发的更新,需要标记变量didScheduleRenderPhaseUpdate,后续单独处理。
再来关注:
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。
你可以在这里
(opens new window)看到这段提前计算
state的逻辑
小Tip
我们通常认为,useReducer(reducer, initialState)的传参为初始化参数,在以后的调用中都不可变。
但是在updateReducer方法中,可以看到lastRenderedReducer在每次调用时都会重新赋值。
function updateReducer(reducer, initialArg, init) {// ...queue.lastRenderedReducer = reducer;// ...
也就是说,reducer参数是随时可变的。
reducer可变Demo
每秒useReducer使用的reducer会改变一次
点击按钮后会随时间不同会出现+1或-1的效果
关注公众号,后台回复582获得在线Demo地址
