useState: 哪有有组件,就有他,无处不在!那么useState内部究竟是什么?如何实现数据更新的?
下面,我们来一层层解析,一探究竟吧~
一. useState
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
嗯~,就几行代码,是不是很简单?
如果您觉得很简单,那就掉坑里了......
resolveDispatcher
function resolveDispatcher() {
var dispatcher = ReactCurrentDispatcher.current;
if (!(dispatcher !== null)) {
{
throw Error( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem." );
}
}
return dispatcher;
}
ReactCurrentDispatcher是什么呢? 之前我们在聊useEffect中有提及到。 他是在组件实例化时使用到。
dispatch
dispatch是ReactCurrentDispatcher.current指向。其数据结构如下:
{
useState: function (initialState) {
currentHookNameInDev = 'useState';
mountHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
// ...其他方法,比如useEffect
}
mountState
function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
initialState是我们useState的初始值,可以看到初始值可以是一个字符串也可以是个函数。
hook对象如下:
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
注意了,这里queue非常重要,这是一个环状链表,在这里看不出,因为next是null。但实际dispatch时构建成环状链表。
最后return [hook.memoizedState, dispatch],这就是useState的真面目了,但记住,这只是初始化阶段。 dispatch就是setState。
二. setState
setState将执行上述方法的dispatch。
思考:
- 同一个state进行多次setState看结果如何?
- 针对不同的state,分别执行setState又将如何?
dispatchAction
function dispatchAction(fiber, queue, action) {
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
var update = {
lane: lane,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
var pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
// ...
var eagerState = lastRenderedReducer(currentState, action);
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
// ...
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
这里很简单了,queue被构建成了环状链表,并且最后一个state更新放到了第一个,通过next一路关联起来。 eagerState即新的state值,到这里为止,多次的state更新都会调用scheduleUpdateOnFiber。
前面的章节有分析scheduleUpdateOnFiber,如果是更新(有别与第一次创建),先进入ensureRootIsScheduled。
ensureRootIsScheduled在上节有分析lane先关调度。同赛道将执行一次调度。
具体怎么调度,可以参考我的schedule源码解析。
最终将执行performSyncWorkOnRoot方法,是不是很熟悉?这里就是首次渲染时执行的核心方法。
当时,我们留下一个问题:为什么performSyncWorkOnRoot开始就要执行flushPassiveEffects呢?
这是因为新的state状态变更,可能导致旧组件的卸载,新组件的生成,或由于上次异步effect未执行完成。
这里又有个问题:为什么要在performSyncWorkOnRoot最后还要执行一次ensureRootIsScheduled?
首次渲染时,并不需要再次执行。这是因为,如果存在多个不同优先级的更新任务时,当完成一个更新之后,再次进入ensureRootIsScheduled获取下一个最高优先级任务,再次调度。 直到清空pendingLanes。
三. 总结
useState在组件实例化时被缓存queue对象,并缓存了dispatch引用。在执行dispatch时,构建环状链表queue。
setState阶段,可以是同步,也可以是异步。本质上他的数据传递不在于此,而是在fiber构建中已绑定。
具体的更新请求,经过lane模型,执行又经过schedule调度。 下面一章节,我们重点聊聊更新中的核心-diff算法。