forked from elecrabbit/front-end-interview
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsetState.md
267 lines (237 loc) · 14.2 KB
/
setState.md
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# setState到底是异步的还是同步的?
点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**.
本文来自于知乎作者[虹晨](https://www.zhihu.com/people/super-huang-59/activities)的[你真的理解setState吗?](https://zhuanlan.zhihu.com/p/39512941),这我见过最能讲清楚setState同步or异步原理的文章了.
---
> 面试官:“react中`setState`是同步的还是异步?”
> 我:“异步的,`setState`不能立马拿到结果。”
> 面试官:“那什么场景下是异步的,可不可能是同步,什么场景下又是同步的”
> 我:“......”
## **setState真的是异步的吗 ?**
这两天自己简单的看了下`setState`的部分实现代码,在这边给到大家一个自己个人的见解,可能文字或图片较多,没耐心的同学可以直接跳过看总结(**源码版本是16.4.1**)。
**看之前,为了方便理解和简化流程,我们默认react内部代码执行到`performWork`、`performWorkOnRoot`、`performSyncWork`、`performAsyncWork`这四个方法的时候,就是react去update更新并且作用到UI上。**
<a name="wSwEC"></a>
## **一、合成事件中的`setState`**
首先得了解一下什么是合成事件,react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在`jsx`中常见的`onClick`、`onChange`这些都是合成事件。
```javascript
class App extends Component {
state = { val: 0 }
increment = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新前的val --> 0
}
render() {
return (
<div onClick={this.increment}>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
```
合成事件中的`setState`写法比较常见,点击事件里去改变`this.state.val`的状态值,在`increment`事件中打个断点可以看到调用栈,这里我贴一张自己画的流程图:<br />![](https://cdn.nlark.com/yuque/0/2019/jpeg/128853/1564565319939-df93f6f8-329d-4775-b3a7-09796d87fa08.jpeg#align=left&display=inline&height=1173&originHeight=643&originWidth=720&size=0&status=done&width=1314)合成事件中setState的调用栈
从 `dispatchInteractiveEvent` 到 `callCallBack` 为止,都是对合成事件的处理和执行,从 `setState` 到 `requestWork` 是调用 `this.setState` 的逻辑,这边主要看下 `requestWork`这个函数(从 `dispatchEvent` 到 `requestWork` 的调用栈是属于 `interactiveUpdates$1` 的 `try` 代码块,下文会提到)。
```javascript
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 {
scheduleCallbackWithExpiration(expirationTime);
}
}
```
在`requestWork`中有三个if分支,三个分支中有两个方法 `performWorkOnRoot` 和`performSyncWork`,就是我们默认的update函数,但是在合成事件中,走的是第二个if分支,第二个分支中有两个标识 `isBatchingUpdates` 和 `isUnbatchingUpdates` 两个初始值都为`false`,但是在 `interactiveUpdates$1` 中会把 `isBatchingUpdates` 设为 `true` ,下面就是 `interactiveUpdates$1` 的代码:
```javascript
function interactiveUpdates$1(fn, a, b) {
if (isBatchingInteractiveUpdates) {
return fn(a, b);
}
// If there are any pending interactive updates, synchronously flush them.
// This needs to happen before we read any handlers, because the effect of
// the previous event may influence which handlers are called during
// this event.
if (!isBatchingUpdates && !isRendering && lowestPendingInteractiveExpirationTime !== NoWork) {
// Synchronously flush pending interactive updates.
performWork(lowestPendingInteractiveExpirationTime, false, null);
lowestPendingInteractiveExpirationTime = NoWork;
}
var previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
var previousIsBatchingUpdates = isBatchingUpdates;
isBatchingInteractiveUpdates = true;
isBatchingUpdates = true; // 把requestWork中的isBatchingUpdates标识改为true
try {
return fn(a, b);
} finally {
isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
}
}
```
在这个方法中把 `isBatchingUpdates` 设为了 `true` , 导致在 `requestWork `方法中, `isBatchingUpdates` 为 `true` ,但是 `isUnbatchingUpdates` 是 `false`,而被直接return了。<br />那return完的逻辑回到哪里呢,最终正是回到了 `interactiveUpdates` 这个方法,仔细看一眼,这个方法里面有个 [try finally](https://link.zhihu.com/?target=http%3A//javascript.ruanyifeng.com/grammar/error.html%23toc12) 语法,前端这个其实是用的比较少的,简单的说就是会先执行 `try`代码块中的语句,然后再执行 `finally` 中的代码,而 `fn(a, b)` 是在try代码块中,刚才说到在`requestWork` 中被return掉的也就是这个fn(上文提到的 `从dispatchEvent`到 `requestWork`的一整个调用栈)。<br />所以当你在`increment`中调用 `setState` 之后去 console.log 的时候,是属于`try`代码块中的执行,但是由于是合成事件,try 代码块执行完 state 并没有更新,所以你输入的结果是更新前的 `state` 值,这就导致了所谓的"异步",但是当你的 try 代码块执行完的时候(也就是你的increment合成事件),这个时候会去执行 `finally` 里的代码,在 `finally` 中执行了 `performSyncWork` 方法,这个时候才会去更新你的 `state`并且渲染到UI上。
<a name="u6F3m"></a>
## **二、生命周期函数中的`setState`**
```javascript
class App extends Component {
state = { val: 0 }
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的还是更新前的值 --> 0
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
```
钩子函数中setState的调用栈:<br />![](https://cdn.nlark.com/yuque/0/2019/jpeg/128853/1564565319857-42664206-6f8b-458d-a663-aeabe53af749.jpeg#align=left&display=inline&height=973&originHeight=492&originWidth=720&size=0&status=done&width=1424)钩子函数中setState的调用栈
其实还是和合成事件一样,当`componentDidmount`执行的时候,react内部并没有更新,执行完`componentDidmount`后才去`commitUpdateQueue`更新。这就导致你在`componentDidmount`中`setState`完去console.log拿的结果还是更新前的值。
<a name="IzIDK"></a>
## **三、原生事件中的`setState`**
```javascript
class App extends Component {
state = { val: 0 }
changeValue = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新后的值 --> 1
}
componentDidMount() {
document.body.addEventListener('click', this.changeValue, false)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
```
原生事件是指非react合成事件,原生自带的事件监听 `addEventListener` ,或者也可以用原生js、jq直接 `document.querySelector().onclick` 这种绑定事件的形式都属于原生事件。<br />![](https://cdn.nlark.com/yuque/0/2019/jpeg/128853/1564565319940-8bb538f5-b39c-4758-a8de-9ba88479f5b6.jpeg#align=left&display=inline&height=951&originHeight=668&originWidth=720&size=0&status=done&width=1025)原生事件中setState的调用栈
原生事件的调用栈就比较简单了,因为没有走合成事件的那一大堆,直接触发click事件,到`requestWork` ,在 `requestWork` 里由于 `expirationTime === Sync` 的原因,直接走了 `performSyncWork` 去更新,并不像合成事件或钩子函数中被return,所以当你在原生事件中setState后,能同步拿到更新后的state值。
## **四、setTimeout中的`setState`**
```javascript
class App extends Component {
state = { val: 0 }
componentDidMount() {
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出更新后的值 --> 1
}, 0)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
```
在 `setTimeout` 中去 `setState` 并不算是一个单独的场景,它是随着你外层去决定的,因为你可以在合成事件中 `setTimeout`,可以在钩子函数中 `setTimeout`,也可以在原生事件`setTimeout`,但是不管是哪个场景下,基于[event loop](https://link.zhihu.com/?target=https%3A//www.youtube.com/watch%3Fv%3D6XRNXXgP_0)的模型下,`setTimeout` 中里去 `setState` 总能拿到最新的state值。<br />举个栗子,比如之前的合成事件,由于 `setTimeout(_ => { this.setState()}, 0)`是在`try` 代码块中,当你 `try` 代码块执行到`setTimeout`的时候,把它丢到列队里,并没有去执行,而是先执行的 `finally` 代码块,等 `finally` 执行完了,`isBatchingUpdates`又变为了 `false`,导致最后去执行队列里的 `setState` 时候, `requestWork` 走的是和原生事件一样的`expirationTime === Sync` if分支,所以表现就会和原生事件一样,可以同步拿到最新的state值。
## **五、`setState`中的批量更新**
```javascript
class App extends Component {
state = { val: 0 }
batchUpdates = () => {
this.setState({ val: this.state.val + 1 })
this.setState({ val: this.state.val + 1 })
this.setState({ val: this.state.val + 1 })
}
render() {
return (
<div onClick={this.batchUpdates}>
{`Counter is ${this.state.val}`} // 1
</div>
)
}
}
```
上面的结果最终是1,在`setState`的时候react内部会创建一个`updateQueue`,通过`firstUpdate`、`lastUpdate`、`lastUpdate.next`去维护一个更新的队列,在最终的`performWork`中,相同的key会被覆盖,只会对最后一次的`setState`进行更新,下面是部分实现代码:
```javascript
function createUpdateQueue(baseState) {
var queue = {
expirationTime: NoWork,
baseState: baseState,
firstUpdate: null,
lastUpdate: null,
firstCapturedUpdate: null,
lastCapturedUpdate: null,
firstEffect: null,
lastEffect: null,
firstCapturedEffect: null,
lastCapturedEffect: null
};
return queue;
}
function appendUpdateToQueue(queue, update, expirationTime) {
// Append the update to the end of the list.
if (queue.lastUpdate === null) {
// Queue is empty
queue.firstUpdate = queue.lastUpdate = update;
} else {
queue.lastUpdate.next = update;
queue.lastUpdate = update;
}
if (queue.expirationTime === NoWork || queue.expirationTime > expirationTime) {
// The incoming update has the earliest expiration of any update in the
// queue. Update the queue's expiration time.
queue.expirationTime = expirationTime;
}
}
```
## **最后看个🌰**
```javascript
class App extends React.Component {
state = { val: 0 }
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val);
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
}, 0)
}
render() {
return <div>{this.state.val}</div>
}
}
```
结合上面分析的,钩子函数中的 `setState` 无法立马拿到更新后的值,所以前两次都是输出0,当执行到`setTimeout`里的时候,前面两个state的值已经被更新,由于 `setState` 批量更新的策略, `this.state.val` 只对最后一次的生效,为1,而在 `setTimmout` 中`setState` 是可以同步拿到更新结果,所以 `setTimeout` 中的两次输出2,3,最终结果就为`0, 0, 2, 3`。
## **总结 :**
1. **`setState`只在合成事件和钩子函数中是“异步”的,在原生事件和`setTimeout` 中都是同步的。**
2. **`setState` 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 `setState(partialState, callback)` 中的`callback`拿到更新后的结果。**
3. **`setState` 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次`setState`,`setState`的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时`setState`多个不同的值,在更新时会对其进行合并批量更新。**
以上就是我看了部分代码后的粗浅理解,对源码细节的那块分析的较少,主要是想让大家了解`setState`在不同的场景,不同的写法下到底发生了什么样的一个过程和结果,希望对大家有帮助,由于是个人的理解和见解,如果哪里有说的不对的地方,欢迎大家一起指出并讨论。
---
## 公众号
想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新.
**简历模板:** 关注公众号回复「模板」获取
**《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取
![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png)