useState实现
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方法。
该方法内部会执行FunctionComponent
对应函数(即fiber.type
)。
你可以在这里看到这段逻辑
对于这两个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,useState
会调用mountState。
我们来简单对比这这两个方法:
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 创建并返回当前的hook
const hook = mountWorkInProgressHook();
// ...赋值初始state
// 创建queue
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
// ...创建dispatch
return [hook.memoizedState, dispatch];
}
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 创建并返回当前的hook
const hook = mountWorkInProgressHook();
// ...赋值初始state
// 创建queue
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
// ...创建dispatch
return [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时使用的reducer
lastRenderedReducer: reducer,
// 上一次render时的state
lastRenderedState: (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。
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 获取当前hook
const 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,此时该FunctionComponent
对应的fiber
以及hook.queue
已经通过调用bind
方法预先作为参数传入。
function dispatchAction(fiber, queue, action) {
// ...创建update
var update = {
eventTime: eventTime,
lane: lane,
suspenseConfig: suspenseConfig,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
// ...将update加入queue.pending
var 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
。
你可以在这里看到这段提前计算
state
的逻辑
小Tip
我们通常认为,useReducer(reducer, initialState)
的传参为初始化参数,在以后的调用中都不可变。
但是在updateReducer
方法中,可以看到lastRenderedReducer
在每次调用时都会重新赋值。
function updateReducer(reducer, initialArg, init) {
// ...
queue.lastRenderedReducer = reducer;
// ...
也就是说,reducer
参数是随时可变的。
::: details reducer可变demo
每秒useReducer
使用的reducer
会改变一次
点击按钮后会随时间不同会出现+1
或-1
的效果
:::