本文在我的 Github博客 https://github.com/Jacky-Summer/personal-blog 同步更新
前言本篇寫的 setState(涉及源碼部分)是針對 React15 版本,即是沒有 Fiber 介入的;為了方便看和寫,所以選擇舊版本,Fiber 寫起來有點難,先留著將會寫。setState 在 React 15 的原理能理解,16 版本的也是大同小異。
雖然已經用 React Hooks 很久了,React15 的this.setState()形式都很少用了,但依然是站在回顧與總結的角度,看待 React 的變遷和發展,所以最近開始重新回顧以前一知半解的一些原理問題,慢慢沉澱技術。
setState 經典問題setState(updater, [callback])React 通過 this.setState() 來更新 state,當使用 this.setState()的時候 ,React 會調用 render 方法來重新渲染 UI。
setState 的幾種用法就不用我說了,來看看網上討論 setState 比較多的問題:
批量更新import React, { Component } from 'react'
class App extends Component {
state = {
count: 1,
}
handleClick = () => {
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
}
render() {
return (
<>
<button onClick={this.handleClick}>加1</button>
<div>{this.state.count}</div>
</>
)
}
}
export default App點擊按鈕觸發事件,列印的都是 1,頁面顯示 count 的值為 2。
這就是常常說的 setState 批量更新,對同一個值進行多次 setState , setState 的批量更新策略會對其進行覆蓋,取最後一次的執行結果。所以每次 setState 之後立即列印值都是初始值 1,而最後頁面顯示的值則為最後一次的執行結果,也就是 2。
setTimeoutimport React, { Component } from 'react'
class App extends Component {
state = {
count: 1,
}
handleClick = () => {
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
setTimeout(() => {
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 3
})
}
render() {
return (
<>
<button onClick={this.handleClick}>加1</button>
<div>{this.state.count}</div>
</>
)
}
}
export default App點擊按鈕觸發事件,發現 setTimeout 裡面的 count 值列印值為 3,頁面顯示 count 的值為 3。setTimeout 裡面 setState 之後能馬上能到最新值。
在 setTimeout 裡面,setState 是同步的;經過前面兩次的 setState 批量更新,count 值已經更新為 2。在 setTimeout 裡面的首先拿到新的 count 值 2,再一次 setState,然後能實時拿到 count 的值為 3。
DOM 原生事件import React, { Component } from 'react'
class App extends Component {
state = {
count: 1,
}
componentDidMount() {
document.getElementById('btn').addEventListener('click', this.handleClick)
}
handleClick = () => {
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 2
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 3
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 4
}
render() {
return (
<>
<button id='btn'>觸發原生事件</button>
<div>{this.state.count}</div>
</>
)
}
}
export default App點擊按鈕,會發現每次 setState 列印出來的值都是實時拿到的,不會進行批量更新。
在 DOM 原生事件裡面,setState 也是同步的。
setState 同步異步問題這裡討論的同步和異步並不是指 setState 是否異步執行,使用了什麼異步代碼,而是指調用 setState 之後 this.state 能否立即更新。
React 中的事件都是合成事件,都是由 React 內部封裝好的。React 本身執行的過程和代碼都是同步的,只是合成事件和鉤子函數的調用順序在更新之前,導致在合成事件和鉤子函數中沒法立馬拿到更新後的值,就是我們所說的"異步"了。
由上面也可以得知 setState 在原生事件和 setTimeout 中都是同步的。
setState 源碼層面源碼選擇的 React 版本為15.6.2
setState 函數源碼裡面,setState 函數的代碼
React 組件繼承自React.Component,而 setState 是React.Component的方法
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState)
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState')
}
}可以看到它直接調用了 this.updater.enqueueSetState 這個方法。
enqueueSetStateenqueueSetState: function(publicInstance, partialState) {
// 拿到對應的組件實例
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'setState',
);
// queue 對應一個組件實例的 state 數組
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState); // 將 partialState 放入待更新 state 隊列
// 處理當前的組件實例
enqueueUpdate(internalInstance);
}_pendingStateQueue表示待更新隊列
enqueueSetState 做了兩件事:
用 enqueueUpdate 來處理將要更新的實例對象。接下來看看 enqueueUpdate 做了什麼:
function enqueueUpdate(component) {
ensureInjected()
// isBatchingUpdates 標識著當前是否處於批量更新過程
if (!batchingStrategy.isBatchingUpdates) {
// 若當前沒有處於批量創建/更新組件的階段,則立即更新組件
batchingStrategy.batchedUpdates(enqueueUpdate, component)
return
}
// 需要批量更新,則先把組件塞入 dirtyComponents 隊列
dirtyComponents.push(component)
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1
}
}batchingStrategy 表示批量更新策略,isBatchingUpdates表示當前是否處於批量更新過程,默認是 false。
enqueueUpdate做的事情:
判斷組件是否處於批量更新模式,如果是,即isBatchingUpdates為 true 時,不進行 state 的更新操作,而是將需要更新的組件添加到dirtyComponents數組中;
如果不是處於批量更新模式,則對所有隊列中的更新執行batchedUpdates方法
當中 batchingStrategy該對象的isBatchingUpdates屬性直接決定了是馬上要走更新流程,還是應該進入隊列等待;所以大概可以得知batchingStrategy用於管控批量更新的對象。
來看看它的源碼:
/**
* batchingStrategy源碼
**/
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false, // 初始值為 false 表示當前並未進行任何批量更新操作
// 發起更新動作的方法
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates
ReactDefaultBatchingStrategy.isBatchingUpdates = true
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e)
} else {
// 啟動事務,將 callback 放進事務裡執行
return transaction.perform(callback, null, a, b, c, d, e)
}
},
}「每當 React 調用 batchedUpdate 去執行更新動作時,會先把isBatchingUpdates置為 true,表明正處於批量更新過程中。」
看完批量更新整體的管理機制,發現還有一個操作是transaction.perform,這就引出 React 中的 Transaction(事務)機制。
Transaction(事務)機制Transaction 是創建一個黑盒,該黑盒能夠封裝任何的方法。因此,那些需要在函數運行前、後運行的方法可以通過此方法封裝(即使函數運行中有異常拋出,這些固定的方法仍可運行)。
在 React 中源碼有關於 Transaction 的注釋如下:
* <pre>
* wrappers (injected at creation time)
* + +
* | |
* +--|---|----+
* | v | |
* | ++ | |
* | +--| wrapper1 |---|----+ |
* | | ++ v | |
* | | +---+ | |
* | | +----| wrapper2 |---+ |
* | | | +---+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +----+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +-->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +----+ +---+ +---+ |
* | initialize close |
* +-+
* </pre>根據以上注釋,可以看出:一個 Transaction 就是將需要執行的 method 使用 wrapper(一組 initialize 及 close 方法稱為一個 wrapper) 封裝起來,再通過 Transaction 提供的 perform 方法執行。
在 perform 之前,先執行所有 wrapper 中的 initialize 方法;perform 完成之後(即 method 執行後)再執行所有的 close 方法,而且 Transaction 支持多個 wrapper 疊加。這就是 React 中的事務機制。
batchingStrategy 批量更新策略再看回batchingStrategy批量更新策略,ReactDefaultBatchingStrategy 其實就是一個批量更新策略事務,它的 wrapper 有兩個:FLUSH_BATCHED_UPDATES 和 RESET_BATCHED_UPDATES。
isBatchingUpdates在 close 方法被復位為 false,如下代碼:
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function () {
ReactDefaultBatchingStrategy.isBatchingUpdates = false
},
}
// flushBatchedUpdates 將所有的臨時 state 合併並計算出最新的 props 及 state
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
}
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]「React 鉤子函數」
都說 React 鉤子函數也是異步更新,則必須一開始 isBatchingUpdates 為 ture,但默認 isBatchingUpdates 為 false,它是在哪裡被設置為 true 的呢?來看下面代碼:
// ReactMount.js
_renderNewRootComponent: function( nextElement, container, shouldReuseMarkup, context ) {
// 實例化組件
var componentInstance = instantiateReactComponent(nextElement);
// 調用 batchedUpdates 方法
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode,
componentInstance,
container,
shouldReuseMarkup,
context
);
}這段代碼是在首次渲染組件時會執行的一個方法,可以看到它內部調用了一次 batchedUpdates 方法(將 isBatchingUpdates 設為 true),這是因為在組件的渲染過程中,會按照順序調用各個生命周期(鉤子)函數。如果在函數裡面調用 setState,則看下列代碼:
if (!batchingStrategy.isBatchingUpdates) {
// 立即更新組件
batchingStrategy.batchedUpdates(enqueueUpdate, component)
return
}
// 批量更新,則先把組件塞入 dirtyComponents 隊列
dirtyComponents.push(component)則所有的更新都能夠進入 dirtyComponents 裡去,即 setState 走的異步更新
「React 合成事件」
當我們在組件上綁定了事件之後,事件中也有可能會觸發 setState。為了確保每一次 setState 都有效,React 同樣會在此處手動開啟批量更新。看下面代碼:
// ReactEventListener.js
dispatchEvent: function (topLevelType, nativeEvent) {
try {
// 處理事件:batchedUpdates會將 isBatchingUpdates設為true
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}isBatchingUpdates 這個變量,在 React 的生命周期函數以及合成事件執行前,已經被 React 改為 true,這時我們所做的 setState 操作自然不會立即生效。當函數執行完畢後,事務的 close 方法會再把 isBatchingUpdates 改為 false。
就像最上面的例子,整個過程模擬大概是:
handleClick = () => {
// isBatchingUpdates = true
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
// isBatchingUpdates = false
}而如果有 setTimeout 介入後
handleClick = () => {
// isBatchingUpdates = true
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
setTimeout(() => {
// setTimeout異步執行,此時 isBatchingUpdates 已經被重置為 false
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 3
})
// isBatchingUpdates = false
}isBatchingUpdates是在同步代碼中變化的,而 setTimeout 的邏輯是異步執行的。當 this.setState 調用真正發生的時候,isBatchingUpdates 早已經被重置為 false,這就使得 setTimeout 裡面的 setState 具備了立刻發起同步更新的能力。
batchedUpdates 方法看到這裡大概就可以了解 setState 的同步異步機制了,接下來讓我們進一步體會,可以把 React 的batchedUpdates拿來試試,在該版本中此方法名稱被置為unstable_batchedUpdates即不穩定的方法。
import React, { Component } from 'react'
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'
class App extends Component {
state = {
count: 1,
}
handleClick = () => {
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 1
setTimeout(() => {
batchedUpdates(() => {
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 2
})
})
}
render() {
return (
<>
<button onClick={this.handleClick}>加1</button>
<div>{this.state.count}</div>
</>
)
}
}
export default App如果調用batchedUpdates方法,則 isBatchingUpdates變量會被設置為 true,由上述得為 true 走的是批量更新策略,則 setTimeout 裡面的方法也變成異步更新了,所以最終列印值為 2,與本文第一道題結果一樣。
總結setState 同步異步的表現會因調用場景的不同而不同:在 React 鉤子函數及合成事件中,它表現為異步;而在 setTimeout/setInterval 函數,DOM 原生事件中,它都表現為同步。這是由 React 事務機制和批量更新機制的工作方式來決定的。
在 React16 中,由於引入了 Fiber 機制,源碼多少有點不同,但大同小異,之後我也會寫 React16 原理的文章,敬請關注!
❝歡迎關注我掘金帳號和Github技術博客:
掘金:https://juejin.im/user/1257497033714477Github:https://github.com/Jacky-Summer覺得對你有幫助或有啟發的話歡迎 star,你的鼓勵是我持續創作的動力~如需在微信公眾號平臺轉載請聯繫作者授權同意,其它途徑轉載請在文章開頭註明作者和文章出處。❞