面試官:「react中
setState
是同步的仍是異步?」
我:「異步的,setState
不能立馬拿到結果。」javascript
面試官:「那什麼場景下是異步的,可不多是同步,什麼場景下又是同步的?」
我:「......」html
setState
真的是異步的嗎?這兩天本身簡單的看了下 setState
的部分實現代碼,在這邊給到你們一個本身我的的看法,可能文字或圖片較多,沒耐心的同窗能夠直接跳過看總結(源碼版本是16.4.1)。前端
看以前,爲了方便理解和簡化流程,咱們默認react內部代碼執行到performWork
、performWorkOnRoot
、performSyncWork
、performAsyncWork
這四個方法的時候,就是react去update更新而且做用到UI上。java
setState
首先得了解一下什麼是合成事件,react爲了解決跨平臺,兼容性問題,本身封裝了一套事件機制,代理了原生的事件,像在jsx
中常見的onClick
、onChange
這些都是合成事件。react
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
事件中打個斷點能夠看到調用棧,這裏我貼一張本身畫的流程圖:git
dispatchInteractiveEvent
到
callCallBack
爲止,都是對合成事件的處理和執行,從
setState
到
requestWork
是調用
this.setState
的邏輯,這邊主要看下
requestWork
這個函數(從
dispatchEvent
到
requestWork
的調用棧是屬於
interactiveUpdates$1
的
try
代碼塊,下文會提到)。
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
的代碼:github
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了。面試
那return完的邏輯回到哪裏呢,最終正是回到了 interactiveUpdates
這個方法,仔細看一眼,這個方法裏面有個try finally語法,前端同窗這個實際上是用的比較少的,簡單的說就是會先執行 try
代碼塊中的語句,而後再執行 finally
中的代碼,而 fn(a, b)
是在try代碼塊中,剛纔說到在 requestWork
中被return掉的也就是這個fn(上文提到的 從dispatchEvent
到 requestWork
的一整個調用棧)。app
因此當你在 increment
中調用 setState
以後去console.log的時候,是屬於 try
代碼塊中的執行,可是因爲是合成事件,try代碼塊執行完state並無更新,因此你輸入的結果是更新前的 state
值,這就致使了所謂的"異步",可是當你的try代碼塊執行完的時候(也就是你的increment合成事件),這個時候會去執行 finally
裏的代碼,在 finally
中執行了 performSyncWork
方法,這個時候纔會去更新你的 state
而且渲染到UI上。less
setState
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的調用棧:
其實仍是和合成事件同樣,當componentDidmount
執行的時候,react內部並無更新,執行完
componentDidmount
後纔去
commitUpdateQueue
更新。這就致使你在
componentDidmount
中
setState
完去console.log拿的結果仍是更新前的值。
setState
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
這種綁定事件的形式都屬於原生事件。
requestWork
,在
requestWork
裏因爲
expirationTime === Sync
的緣由,直接走了
performSyncWork
去更新,並不像合成事件或鉤子函數中被return,因此當你在原生事件中setState後,能同步拿到更新後的state值。
setState
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的模型下, setTimeout
中裏去 setState
總能拿到最新的state值。
舉個栗子,好比以前的合成事件,因爲你是 setTimeout(_ => { this.setState()}, 0)
是在 try
代碼塊中,當你 try
代碼塊執行到 setTimeout
的時候,把它丟到列隊裏,並無去執行,而是先執行的 finally
代碼塊,等 finally
執行完了, isBatchingUpdates
又變爲了 false
,致使最後去執行隊列裏的 setState
時候, requestWork
走的是和原生事件同樣的 expirationTime === Sync
if分支,因此表現就會和原生事件同樣,能夠同步拿到最新的state值。
setState
中的批量更新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
進行更新,下面是部分實現代碼:
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;
}
}
複製代碼
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
。
setState
只在合成事件和鉤子函數中是「異步」的,在原生事件和 setTimeout
中都是同步的。setState
的「異步」並非說內部由異步代碼實現,其實自己執行的過程和代碼都是同步的,只是合成事件和鉤子函數的調用順序在更新以前,致使在合成事件和鉤子函數中無法立馬拿到更新後的值,形式了所謂的「異步」,固然能夠經過第二個參數 setState(partialState, callback) 中的callback拿到更新後的結果。setState
的批量更新優化也是創建在「異步」(合成事件、鉤子函數)之上的,在原生事件和setTimeout 中不會批量更新,在「異步」中若是對同一個值進行屢次 setState
, setState
的批量更新策略會對其進行覆蓋,取最後一次的執行,若是是同時 setState
多個不一樣的值,在更新時會對其進行合併批量更新。以上就是我看了部分代碼後的粗淺理解,對源碼細節的那塊分析的較少,主要是想讓你們理解setState
在不一樣的場景,不一樣的寫法下到底發生了什麼樣的一個過程和結果,但願對你們有幫助,因爲是我的的理解和看法,若是哪裏有說的不對的地方,歡迎你們一塊兒指出並討論。
有好友整理了一波內推崗位,已發佈到github,感興趣的能夠聯繫cXE3MjcwNDAxNDE=