持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
如果你想要学习怎样使用 useState,可以看上一篇👉 02|在 React 中正确使用 useState 的姿势
这一篇是作为上一篇的补充,主要是通过深入源码来解释一些问题,比如经典面试题 “React 中的 setState 是同步的还是异步的?”。如果你精力有限的话,可以跳过本篇,毕竟工作不需要掌握源码,当你感兴趣了,再来看也不迟。
// ...
const [num, setNum] = React.useState(0)
const add = () => {
setNum(num + 1)
console.log(num) // 打印出来的是 num + 1 之前的值
setNum(num + 2)
console.log(num) // 打印出来的仍是 num + 1 之前的值
}
//...
在上面的代码中我们发现,每次调用add都会将 num + 2,并且 console.log 出来的都是上一次的值,我们通过解析 React 中的源码来理解这个问题。
一、首次渲染
温馨提示:当前debug 的 React 版本为 18.1.0。 最好你能有链表的知识,会更容易理解~
下面这是一段在React首次渲染时调用你写的 const [num, setNum] = useState(0) 的 useState时最终会调用的函数,建议复制到编辑器中查看,或者在 codesandbox 中打开查看 效果更佳。
function mountState(initialState) {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 注意这里 queue 是挂在hook上的,(hook 其实是跟 App 组件关联着的)
const queue = (hook.queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
});
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber, // App 这个组件 fiber
queue,
)));
// **** 3 ****
return [hook.memoizedState, dispatch];
}
因为我们是首次执行,所以会进入到mountState内,我们只需要看下面这三个地方:
-
line7 这里将
initialState赋值给hook.memoizedState -
line18,创建一个
dispatch函数,本质是dispatchAction,这里事先会传两个参数,你就当作为了定位将来是什么地方触发了这个函数即可 -
line24,返回一个数组,内容也就是我们传入的
initialState与一个dispatch
二、点击触发事件
在点击button去更新时(也就是去更新num时),我下面只展示部分重要的代码 (代码太长,并且掘金的代码展示不够友好,建议复制到编辑器中查看,或者在 codesandbox 中打开查看效果更佳)
function dispatchSetState(fiber, queue, action) {
const update = {
lane: lane,
action: action, // 新的state
eagerReducer: null,
eagerState: null,
next: null
};
const _pending = queue.pending;
if (_pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = _pending.next;
_pending.next = update;
}
queue.pending = update;
var alternate = fiber.alternate;
// 因为是当前fiber的第一个更新,所以会进入这个条件判断,直接看 line35
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
var lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
var prevDispatcher;
try {
var currentState = queue.lastRenderedState;
// lastRenderedReducer 内判断 action 是 function,就会执行 action(currentState),返回新的state
// action 也就是 setNum(n => n + 1) 中的函数
var eagerState = lastRenderedReducer(currentState, action); // Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true;
update.eagerState = eagerState;
if (objectIs(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
} catch (error) {// Suppress the error. It will throw again in the render phase.
} finally {
{
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
}
}
var root = scheduleUpdateOnFiber(fiber, lane, eventTime);
// ......
}
其中fiber和queue就是之前的dispatchSetState中先传过来的两个值,action就是本次更新进来的新数据。
-
第一个
setNum(本质上是dispatchSetState)接收新的值(1),也就是dispatchSetState中的action -
line 13 此时的
pending是null(记得mountState中设置的吗?),所以update.next = update(设置成一个环形链表) -
line21,将新的值放到
queue上(queue属于fiber对象上) -
因为是第一个更新,所以 line25 条件判断成功,会在 line44 将
update.hasEagerState = true; update.eagerState = eagerState(更新后的值);(注意这里,后面会使用到) -
下面会调用
scheduleUpdateOnFiber,在这里会标记需要 render -
开始调用第二个
setNum,跟上面的步骤是一样的,其中第 2 点和第 4 点不同a. line13 此时的
pending是存在的 ,将这次的更新放到链表,那么两次更新就都到了queue.pending中了b. line26 当前 fiber 是已经存在了lane,所以不会进入到这里。
add执行完以后,React 会根据标记进行 render 重新执行 App 这个函数(请你不要认为onClick就是执行完我们写的add就完事了,onClick是被 React 代理的,执行完我们的add以后,React 还会针对事件做处理的)
三、再次渲染
- 再次来到
const [num, setNum] = React.useState(0)这里,执行这个useState
useState: function (initialState) {
currentHookNameInDev = 'useState';
updateHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateState(initialState);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
我们可以看到,这里执行并返回updateState(initialState),实际上,updateState最终执行的就是下面的这个updateReducer,如果你对这一大段代码不感兴趣,或看下面的解析仍看不明白,你看最后的函数返回也行。(同样建议复制到编辑器中查看,或在 codesandbox 中查看)
function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook(); // 根据当前的 fiber(App)拿到当前的这个 useState
var queue = hook.queue; // queue 就是上面截图的那个浏览器调试中的 queue
queue.lastRenderedReducer = reducer;
var current = currentHook; // The last rebase update that is NOT part of the base state.
var baseQueue = current.baseQueue; // The last pending update that hasn't been processed yet.
// pendingQueue 就是等待更新的 state 信息
var pendingQueue = queue.pending;
if (pendingQueue !== null) { // 更新队列不为空,也就意味着有更新的需要
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) { // baseQueue 此处等于 null
// Merge the pending queue and the base queue.
var baseFirst = baseQueue.next;
var pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue; // 将 baseQueue 更新
queue.pending = null; // 清空更新队列
}
if (baseQueue !== null) { // 此时 baseQueue 已经是不为空的了
// We have a queue to process.
// baseQueue 是最新的一个更新,也就是 setNum(n => n + 2) 的那个
// 又因为这是一个环形链表,所以它的 next 也就是最早的那个 setNum(n => n + 1)
// 所以这里 first 就是最早的那个更新
var first = baseQueue.next;
// baseState是更新之前的值,如果是第一个onclick的话也就是 0
var newState = current.baseState;
var newBaseState = null;
var newBaseQueueFirst = null;
var newBaseQueueLast = null;
var update = first;
do {
var updateLane = update.lane;
// This update does have sufficient priority.
// 优先级很高的情况会被放到前面,不考虑这里
if (newBaseQueueLast !== null) {
var _clone = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null
};
newBaseQueueLast = newBaseQueueLast.next = _clone;
} // Process this update.
// 第一个更新时 hasEagerState 是已经被设置成了 true的
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = update.eagerState; // 直接设置成新的 state
} else {
// 后续的重复 setState 都走这里
var action = update.action;
// action(setState的形参)如果是一个function时,reducer 里面会执行 action(newState) 否则直接返回 action
// 所以 setState 里面传 function 时,里面的function通过参数能拿到的 state 都是前面 setState 的值
newState = reducer(newState, action);
}
update = update.next; // 下一个更新
} while (update !== null && update !== first); // 从第一个setState跑到最后一个setState
if (newBaseQueueLast === null) {
newBaseState = newState; // 设置最新的 State
} else {
newBaseQueueLast.next = newBaseQueueFirst;
} // Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!objectIs(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate(); // 标记为需要更新
}
hook.memoizedState = newState; // 新的 state
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast; // 更新队列为 null 了
queue.lastRenderedState = newState;
}
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch]; // 最后返回新的 state 和 dispatch
}
-
根据当前的 fiber(App)拿到 hook(useState),判断
pendingQueue是否为空,这里我们这个hook是需要更新的,进入判断 -
进入 line40 的 do while,直接看 line 60,第一个更新时 hasEagerState 是已经被设置成了 true的,在这里设置新的 state。后续循环时就走false了,逻辑一样,也是拿到本次 setState 的值
-
到 line72 将
update设置成下一个setState的信息 -
line 73 的
while会判断如果update存在,并且update与上一个更新不相等的话会再进入一边循环(这里就是判断后面还是否有setState需要去处理) -
最后一顿操作
return [hook.memoizedState, dispatch],也就是这里会返回新的num和setNum供后面使用。
解答
所以,我们再回到问题 React 中的setState 是同步的还是异步的? 那么现在我们有了答案👇
其实,从本质上讲根本就没有什么同步异步一说,只是 React 会为了性能着想,将多次的 setState 放到一个更新队列里面,挂在这个 hook 上,在再次 useState 时(render时),通过 fiber 找到对应的 hook,如果这个 hook 上存在着这个更新队列,则将其合并成为一个,最后返回新的 state 和 dispatch。
其中,如果第一个 setState 中传入的是 function,会在第一次 setState 时就执行里面的函数,后续的 setState (与第一个传入的是不是function无关)会在再次 useState 时合并操作时依次调用 setState 中的 function,function 里面传入的 state 是当前 setState 之前的最新值。(可以看 line69)
这篇真的写的好幸苦啊,如果你觉得还不错的话,希望你能给我点个友好的赞👍 给予我鼓励,谢谢~