React setState 部分源码解析

前言

当我刚开始用 React 的时候,只知道setState异步的,但是问了老大之后才知道也有可能是同步,但是没有细问为啥,想起就来看看源码怎么做的吧,以下源码版本为 16.8.6

setState 关键源码

setState的调用过程是setState->this.updater.enqueueSetState->scheduleWork->requestWork,最关键的地方就是requestWork
由于只希望搞清楚为什么同步异步,所以没那么重要的地方我没怎么看……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function requestWork(root, expirationTime) {
addRootToSchedule(root, expirationTime);

if (isRendering) {
// Prevent reentrancy. Remaining work will be scheduled at the end of
// the currently rendering batch.
return;
}

if (isBatchingUpdates) {
// Flush work at the end of the batch.
if (isUnbatchingUpdates) {
// ...unless we're inside unbatchedUpdates, in which case we should
// flush it now.
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, false);
}

return;
} // TODO: Get rid of Sync and use current time?

if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}

requestWork里面有三个分支,第一个判断isRendering,直接return,第二个判断isBatchingUpdatesisUnbatchingUpdates两个变量,都为false也是return,第三个判断expirationTime === Sync就执行performSyncWork,这其实就是关键的执行任务队列,所以其实setState实际上不是异步,只是代码执行顺序不同,有了异步的感觉。

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class App extends React.Component {
state = { count: 0 };

constructor() {
super();
console.log("constructor");
}
static getDerivedStateFromProps(nextProps, prevState) {
console.log("getDerivedStateFromProps");
}
getSnapshotBeforeUpdate() {
console.log("getSnapshotBeforeUpdate");
}
componentDidMount() {
console.log("componentDidMount");

this.setState({ count: this.state.count + 1 });
console.log(this.state.count);

this.setState({ count: this.state.count + 1 });
console.log(this.state.count);

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);
}, 0);
}

increment = () => {
console.log("increment");
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
};

render() {
console.log("render");
return <div onClick={this.increment}>{this.state.count}</div>;
}
}

测试就用上面代码

componentDidMount 过程

在初始化的过程中会执行performWorkOnRoot方法,而在这里标记了isRendering = true,之后constructor -> getDerivedStateFromProps -> render -> componentDidMount,到了componentDidMount因为isRendering = true会走第一个分支,所以不是马上修改,是异步的。
所以两个setState任务放到队列中,只有后面一个有效。

1
2
3
4
5
function performWorkOnRoot(root, expirationTime, isYieldy) {
!!isRendering ? invariant(false, 'performWorkOnRoot was called recursively. This error is likely caused by a bug in React. Please file an issue.') : void 0;
isRendering = true;
...
}

onClick => increment 过程

React 的合成事件调用十分复杂,到达requestWork的流程是:

  1. dispatchInteractiveEvent`
  2. interactiveUpdates
  3. interactiveUpdates$1

    到这里先停下看看interactiveUpdates$1里面做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function interactiveUpdates$1(fn, a, b) {
...

var previousIsBatchingUpdates = isBatchingUpdates;
isBatchingUpdates = true;

try {
return scheduler.unstable_runWithPriority(
scheduler.unstable_UserBlockingPriority,
function() {
return fn(a, b);
}
);
} finally {
isBatchingUpdates = previousIsBatchingUpdates;

if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
}
}

里面先记录了isBatchingUpdates,然后设置为true然后执行fnfinally还原isBatchingUpdatesisBatchingUpdatesisRendering的状态下执行同步任务

  1. dispatchInteractiveEvent
  2. interactiveUpdates
  3. interactiveUpdates$1
  4. unstable_runWithPriority
  5. fn
  6. dispatchEvent
  7. batchedUpdates
  8. batchedUpdates$1

batchedUpdates$1这边也是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function batchedUpdates$1(fn, a) {
var previousIsBatchingUpdates = isBatchingUpdates;
isBatchingUpdates = true;

try {
return fn(a);
} finally {
isBatchingUpdates = previousIsBatchingUpdates;

if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
}
}

然后继续

  1. dispatchInteractiveEvent
  2. interactiveUpdates
  3. interactiveUpdates$1
  4. unstable_runWithPriority
  5. fn
  6. dispatchEvent
  7. batchedUpdates
  8. batchedUpdates$1
  9. handleTopLevel
  10. runExtractedEventsInBatch
  11. runEventsInBatch
  12. forEachAccumulated
  13. executeDispatchesAndReleaseTopLevel
  14. executeDispatchesAndRelease
  15. executeDispatchesInOrder
  16. executeDispatch
  17. invokeGuardedCallbackAndCatchFirstError
  18. invokeGuardedCallback
  19. invokeGuardedCallbackDev
  20. callCallBack
  21. increment

妈耶,终于到了,到这边依旧是setState,然后又回到了requestWork,到这边就发现到了第二个分支,因为isBatchingUpdates已经被设置了两次true,然后回到batchedUpdates$1,由于设置之前还是true,在这边并不会执行,而是再之前的interactiveUpdates$1

setTimeout 过程

setTimeout 是属于宏任务,再回来的的时候已经脱离componentDidMount的调用栈,所以这边并没有isRenderingisBatchingUpdates,只能走第三个或者,第三个也走不了了。
requestWork第三个条件是expirationTime === Sync,那么这个expirationTime实际上是什么呢?在哪设置的呢?
其实就是在this.updater.enqueueSetState里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enqueueSetState: function(inst, payload, callback) {
var fiber = get(inst);
var currentTime = requestCurrentTime();
var expirationTime = computeExpirationForFiber(currentTime, fiber);
var update = createUpdate(expirationTime);
update.payload = payload;

if (callback !== undefined && callback !== null) {
{
warnOnInvalidCallback$1(callback, "setState");
}
update.callback = callback;
}

flushPassiveEffects();
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
};

computeExpirationForFiber里面是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function computeExpirationForFiber(currentTime, fiber) {
var priorityLevel = scheduler.unstable_getCurrentPriorityLevel();
var expirationTime = void 0;

if ((fiber.mode & ConcurrentMode) === NoContext) {
// Outside of concurrent mode, updates are always synchronous.
expirationTime = Sync;
} else if (isWorking && !isCommitting$1) {
// During render phase, updates expire during as the current render.
expirationTime = nextRenderExpirationTime;
} else {
switch (priorityLevel) {
case scheduler.unstable_ImmediatePriority:
expirationTime = Sync;
break;

case scheduler.unstable_UserBlockingPriority:
expirationTime = computeInteractiveExpiration(currentTime);
break;

case scheduler.unstable_NormalPriority:
// This is a normal, concurrent update
expirationTime = computeAsyncExpiration(currentTime);
break;

case scheduler.unstable_LowPriority:
case scheduler.unstable_IdlePriority:
expirationTime = Never;
break;

default:
invariant(
false,
"Unknown priority level. This error is likely caused by a bug in React. Please file an issue."
);
} // If we're in the middle of rendering a tree, do not update at the same
// expiration time that is already rendering.

if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
expirationTime -= 1;
}
} // Keep track of the lowest pending interactive expiration time. This
// allows us to synchronously flush all interactive updates
// when needed.
// TODO: Move this to renderer?

if (
priorityLevel === scheduler.unstable_UserBlockingPriority &&
(lowestPriorityPendingInteractiveExpirationTime === NoWork ||
expirationTime < lowestPriorityPendingInteractiveExpirationTime)
) {
lowestPriorityPendingInteractiveExpirationTime = expirationTime;
}

return expirationTime;
}

看到如果(fiber.mode & ConcurrentMode) === NoContext的话,就是等于Sync,这里引申出一个新的概念,就是ConcurrentMode并发模式,这个其实是今年第二季才开始推行的,React 向外暴露了flushSyncunstable_scheduleCallback两个 API,在它们里面包裹调用setState将会有不同的优先级,这里面就非常复杂了…其实之前的源码里面都有相关的信息,只是我没心情看下去……(怕不是看不懂,没错!
所以这边可以确认了,如果不是ConcurrentMode那都是Sync,所以

结论

  • setState不存在异步,只是调用顺序问题
  • setState批量更新或者同一个值覆盖都是建立在这种异步
  • 只要脱离了生命周期和合成事件,加上不使用ConcurrentMode的前提下,都是同步的!如果你用原生事件,也是同步的!