先来说this.setSate的更新
这是一个老掉牙的问题,我们都知道,在legacy模式下,合成事件中有batchedEventUpdates方法,所以会开启批量更新,所以会在感觉上是异步的。但是如果在异步回调中调用会丢失上下文,进入同步调用的逻辑。
setTimeout(() => {
this.setState({
count: this.state.count + 1
})
console.log(this.state.count);
this.setState({
count: this.state.count + 1
})
console.log(this.state.count);
this.setState({
count: this.state.count + 1
})
console.log(this.state.count);
})
但是在新版本react18中concurrent不管是否有上下文,都是异步执行的。本文主要讨论concurrent模式下,this.setState是如何更新的。
首先调用setState会执行enqueueSetState
然后会调用
scheduleUpdateOnFiber开始更新。接着会调用ensureRootIsScheduled来判断以什么优先级去更新。
可以看到第一个this.setState会以
NormalPriority来调度整棵树(performConcurrentWorkOnRoot)。并且将root.callbackPriority = newCallbackPriority;
我们来看第二次this.setState。
这一次直接return了,不去调度,因为本次的优先级和上次的优先级一致,所以直接退出了。
这样就可以解释为什么setState在concurrent模式下是异步的,因为只会执行一次performConcurrentWorkOnRoot来调度更新,之后因为优先级是一致的会return掉。
performConcurrentWorkOnRoot会调度一个宏任务来延迟执行(MessageChannel)。
我们再来看看this.setState的更新流程,和如何合并state状态的:
调用链条enqueueUpdate$1->enqueueConcurrentClassUpdate->enqueueUpdate
最后维护了concurrentQueues队列
function finishQueueingConcurrentUpdates() {
var endIndex = concurrentQueuesIndex;
concurrentQueuesIndex = 0;
concurrentlyUpdatedLanes = NoLanes;
var i = 0;
while (i < endIndex) {
var fiber = concurrentQueues[i];
concurrentQueues[i++] = null;
var queue = concurrentQueues[i];
concurrentQueues[i++] = null;
var update = concurrentQueues[i];
concurrentQueues[i++] = null;
var lane = concurrentQueues[i];
concurrentQueues[i++] = null;
if (queue !== null && update !== null) {
var 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;
}
if (lane !== NoLane) {
markUpdateLaneFromFiberToRoot(fiber, update, lane);
}
}
}
然后维护了环状链表。在更新阶段去获取最新的state. 最后在performUnitOfWork中调用updateClassInstance中的processUpdateQueue方法去获取最新的状态 他会遍历环状链表,合并状态。
合并的逻辑在getStateFromUpdate中
function getStateFromUpdate(workInProgress, queue, update, prevState, nextProps, instance) {
switch (update.tag) {
case ReplaceState:
{
var payload = update.payload;
if (typeof payload === 'function') {
// Updater function
{
enterDisallowedContextReadInDEV();
}
var nextState = payload.call(instance, prevState, nextProps);
{
if ( workInProgress.mode & StrictLegacyMode) {
setIsStrictModeForDevtools(true);
try {
payload.call(instance, prevState, nextProps);
} finally {
setIsStrictModeForDevtools(false);
}
}
exitDisallowedContextReadInDEV();
}
return nextState;
} // State object
return payload;
}
case CaptureUpdate:
{
workInProgress.flags = workInProgress.flags & ~ShouldCapture | DidCapture;
}
// Intentional fallthrough
case UpdateState:
{
var _payload = update.payload;
var partialState;
if (typeof _payload === 'function') {
// Updater function
{
enterDisallowedContextReadInDEV();
}
partialState = _payload.call(instance, prevState, nextProps);
{
if ( workInProgress.mode & StrictLegacyMode) {
setIsStrictModeForDevtools(true);
try {
_payload.call(instance, prevState, nextProps);
} finally {
setIsStrictModeForDevtools(false);
}
}
exitDisallowedContextReadInDEV();
}
} else {
// Partial state object
partialState = _payload;
}
if (partialState === null || partialState === undefined) {
// Null and undefined are treated as no-ops.
return prevState;
} // Merge the partial state and the previous state.
return assign({}, prevState, partialState);
}
case ForceUpdate:
{
hasForceUpdate = true;
return prevState;
}
}
return prevState;
}
我们继续讨论useState是如何更新的
import React, { useState } from 'react'
const Index = () => {
const [count, setCount] = useState(1)
const handleClick = () => {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
}
return <div onClick={handleClick}>{count}</div>
}
调用setCount进入dispatchSetState,和this.setState不同的是,进入dispatchSetState会浅比较两次state的值
function dispatchSetState(fiber, queue, action) {
{
if (typeof arguments[3] === 'function') {
error("State updates from the useState() and useReducer() Hooks don't support the " + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().');
}
}
var lane = requestUpdateLane(fiber);
var update = {
lane: lane,
action: action,
hasEagerState: false,
eagerState: null,
next: null
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
var alternate = fiber.alternate;
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;
{
prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
var currentState = queue.lastRenderedState;
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.
// TODO: Do we still need to entangle transitions in this case?
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {// Suppress the error. It will throw again in the render phase.
} finally {
{
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
}
}
var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
var eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane);
}
可以看到,浅比较之后进入了enqueueConcurrentHookUpdate
function enqueueConcurrentHookUpdate(fiber, queue, update, lane) {
var concurrentQueue = queue;
var concurrentUpdate = update;
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
return getRootForUpdatedFiber(fiber);
}
调用了enqueueUpdate和this.setState调用相同维护concurrentQueues队列之后在调用perfomUnitWork前会调用finishQueueingConcurrentUpdates,形成环状链表。
接着就是调用scheduleUpdateOnFiber从根节点调度更新了。之后的流程是一样,包括第二次调用因为优先级相同会直接return。