這是我參與8月更文挑戰的第2天,活動詳情查看:8月更文挑戰css
最近有個朋友面試,面試官問了個奇葩的問題,也就是我寫在標題上的這個問題。html
能問出這個問題,面試官應該對 React 不是很瞭解,也是多是看到面試者簡歷裏面有寫過本身熟悉 React,面試官想經過這個問題來判斷面試者是否是真的熟悉 React 🤣。react
面試官的問題是,setState
是一個宏任務仍是微任務,那麼在他的認知裏,setState
確定是一個異步操做。爲了判斷 setState
究竟是不是異步操做,能夠先作一個實驗,經過 CRA 新建一個 React 項目,在項目中,編輯以下代碼:面試
import React from 'react';
import logo from './logo.svg';
import './App.css';
class App extends React.Component {
state = {
count: 1000
}
render() {
return (
<div className="App"> <img src={logo} alt="logo" className="App-logo" onClick={this.handleClick} /> <p>個人關注人數:{this.state.count}</p> </div>
);
}
}
export default App;
複製代碼
頁面大概長這樣:緩存
上面的 React Logo 綁定了一個點擊事件,如今須要實現這個點擊事件,在點擊 Logo 以後,進行一次 setState
操做,在 set 操做完成時打印一個 log,而且在 set 操做以前,分別添加一個宏任務和微任務。代碼以下:markdown
handleClick = () => {
const fans = Math.floor(Math.random() * 10)
setTimeout(() => {
console.log('宏任務觸發')
})
Promise.resolve().then(() => {
console.log('微任務觸發')
})
this.setState({
count: this.state.count + fans
}, () => {
console.log('新增粉絲數:', fans)
})
}
複製代碼
很明顯,在點擊 Logo 以後,先完成了 setState
操做,而後再是微任務的觸發和宏任務的觸發。因此,setState
的執行時機是早於微任務與宏任務的,即便這樣也只能說它的執行時機早於 Promise.then
,還不能證實它就是同步任務。架構
handleClick = () => {
const fans = Math.floor(Math.random() * 10)
console.log('開始運行')
this.setState({
count: this.state.count + fans
}, () => {
console.log('新增粉絲數:', fans)
})
console.log('結束運行')
}
複製代碼
這麼看,彷佛 setState
又是一個異步的操做。主要緣由是,在 React 的生命週期以及綁定的事件流中,全部的 setState
操做會先緩存到一個隊列中,在整個事件結束後或者 mount 流程結束後,纔會取出以前緩存的 setState
隊列進行一次計算,觸發 state 更新。只要咱們跳出 React 的事件流或者生命週期,就能打破 React 對 setState
的掌控。最簡單的方法,就是把 setState
放到 setTimeout
的匿名函數中。dom
handleClick = () => {
setTimeout(() => {
const fans = Math.floor(Math.random() * 10)
console.log('開始運行')
this.setState({
count: this.state.count + fans
}, () => {
console.log('新增粉絲數:', fans)
})
console.log('結束運行')
})
}
複製代碼
因而可知,setState
本質上仍是在一個事件循環中,並無切換到另外宏任務或者微任務中,在運行上是基於同步代碼實現,只是行爲上看起來像異步。因此,根本不存在面試官的問題。異步
前面的案例中,setState
只有在 setTimeout
中才會變得像一個同步方法,這是怎麼作到的?svg
handleClick = () => {
// 正常的操做
this.setState({
count: this.state.count + 1
})
}
handleClick = () => {
// 脫離 React 控制的操做
setTimeout(() => {
this.setState({
count: this.state.count + fans
})
})
}
複製代碼
先回顧以前的代碼,在這兩個操做中,咱們分別在 Performance 中記錄一次調用棧,看看二者的調用棧有何區別。
在調用棧中,能夠看到 Component.setState
方法最終會調用enqueueSetState
方法 ,而 enqueueSetState
方法內部會調用 scheduleUpdateOnFiber
方法,區別就在於正常調用的時候,scheduleUpdateOnFiber
方法內只會調用 ensureRootIsScheduled
,在事件方法結束後,纔會調用 flushSyncCallbackQueue
方法。而脫離 React 事件流的時候,scheduleUpdateOnFiber
在 ensureRootIsScheduled
調用結束後,會直接調用 flushSyncCallbackQueue
方法,這個方法就是用來更新 state 並從新進行 render。
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
if (lane === SyncLane) {
// 同步操做
ensureRootIsScheduled(root, eventTime);
// 判斷當前是否還在 React 事件流中
// 若是不在,直接調用 flushSyncCallbackQueue 更新
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
} else {
// 異步操做
}
}
複製代碼
上述代碼能夠簡單描述這個過程,主要是判斷了 executionContext
是否等於 NoContext
來肯定當前更新流程是否在 React 事件流中。
衆所周知,React 在綁定事件時,會對事件進行合成,統一綁定到 document
上( react@17
有所改變,變成了綁定事件到 render
時指定的那個 DOM 元素),最後由 React 來派發。
全部的事件在觸發的時候,都會先調用 batchedEventUpdates$1
這個方法,在這裏就會修改 executionContext
的值,React 就知道此時的 setState
在本身的掌控中。
// executionContext 的默認狀態
var executionContext = NoContext;
function batchedEventUpdates$1(fn, a) {
var prevExecutionContext = executionContext;
executionContext |= EventContext; // 修改狀態
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
// 調用結束後,調用 flushSyncCallbackQueue
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
}
}
複製代碼
因此,不論是直接調用 flushSyncCallbackQueue
,仍是推遲調用,這裏本質上都是同步的,只是有個前後順序的問題。
若是你有認真看上面的代碼,你會發如今 scheduleUpdateOnFiber
方法內,會判斷 lane
是否爲同步,那麼是否是存在異步的狀況?
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
if (lane === SyncLane) {
// 同步操做
ensureRootIsScheduled(root, eventTime);
// 判斷當前是否還在 React 事件流中
// 若是不在,直接調用 flushSyncCallbackQueue 更新
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
} else {
// 異步操做
}
}
複製代碼
React 在兩年前,升級 fiber 架構的時候,就是爲其異步化作準備的。在 React 18 將會正式發佈 Concurrent
模式,關於 Concurrent
模式,官方的介紹以下。
什麼是 Concurrent 模式?
Concurrent 模式是一組 React 的新功能,可幫助應用保持響應,並根據用戶的設備性能和網速進行適當的調整。在 Concurrent 模式中,渲染不是阻塞的。它是可中斷的。這改善了用戶體驗。它同時解鎖了之前不可能的新功能。
如今若是想使用 Concurrent
模式,須要使用 React 的實驗版本。若是你對這部份內容感興趣能夠閱讀我以前的文章:《React 架構的演變 - 從同步到異步》。