轉(zhuǎn)自:https://blog.csdn.net/lunahaijiao/article/details/86995969
React 是通過管理狀態(tài)來實(shí)現(xiàn)對(duì)組件的管理廊谓,即使用 this.state 獲取 state刑然,通過 this.setState() 來更新 state港柜,當(dāng)使用 this.setState() 時(shí)晌区,React 會(huì)調(diào)用 render 方法來重新渲染 UI。
首先看一個(gè)例子:
class Example extends React.Component {
? constructor() {
? ? super();
? ? this.state = {
? ? ? val: 0
? ? };
? }
? componentDidMount() {
? ? this.setState({val: this.state.val + 1});
? ? console.log(this.state.val);? ? // 第 1 次 log
? ? this.setState({val: this.state.val + 1});
? ? console.log(this.state.val);? ? // 第 2 次 log
? ? setTimeout(() => {
? ? ? this.setState({val: this.state.val + 1});
? ? ? console.log(this.state.val);? // 第 3 次 log
? ? ? this.setState({val: this.state.val + 1});
? ? ? console.log(this.state.val);? // 第 4 次 log
? ? }, 0);
? }
? render() {
? ? return null;
? }
};
答案是: 0 0 2 3激才,你做對(duì)了嗎该默?
一案糙、setState 異步更新
setState 通過一個(gè)隊(duì)列機(jī)制來實(shí)現(xiàn) state 更新,當(dāng)執(zhí)行 setState() 時(shí)送粱,會(huì)將需要更新的 state 淺合并后放入 狀態(tài)隊(duì)列褪贵,而不會(huì)立即更新 state,隊(duì)列機(jī)制可以高效的批量更新 state抗俄。而如果不通過setState脆丁,直接修改this.state 的值,則不會(huì)放入狀態(tài)隊(duì)列动雹,當(dāng)下一次調(diào)用 setState 對(duì)狀態(tài)隊(duì)列進(jìn)行合并時(shí)槽卫,之前對(duì) this.state 的修改將會(huì)被忽略,造成無法預(yù)知的錯(cuò)誤胰蝠。
React通過狀態(tài)隊(duì)列機(jī)制實(shí)現(xiàn)了 setState 的異步更新歼培,避免重復(fù)的更新 state。
setState(nextState, callback)
1
在 setState 官方文檔中介紹:將 nextState 淺合并到當(dāng)前 state姊氓。這是在事件處理函數(shù)和服務(wù)器請(qǐng)求回調(diào)函數(shù)中觸發(fā) UI 更新的主要方法丐怯。不保證 setState 調(diào)用會(huì)同步執(zhí)行,考慮到性能問題翔横,可能會(huì)對(duì)多次調(diào)用作批處理读跷。
舉個(gè)例子:
// 假設(shè) state.count === 0
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
// state.count === 1, 而不是 3
本質(zhì)上等同于:
// 假設(shè) state.count === 0
Object.assign(state,
? ? ? ? ? ? ? {count: state.count + 1},
? ? ? ? ? ? ? {count: state.count + 1},
? ? ? ? ? ? ? {count: state.count + 1}
? ? ? ? ? ? )
// {count: 1}
但是如何解決這個(gè)問題喃,在文檔中有提到:
也可以傳遞一個(gè)簽名為 function(state, props) => newState 的函數(shù)作為參數(shù)禾唁。這會(huì)將一個(gè)原子性的更新操作加入更新隊(duì)列效览,在設(shè)置任何值之前无切,此操作會(huì)查詢前一刻的 state 和 props。...setState() 并不會(huì)立即改變 this.state 丐枉,而是會(huì)創(chuàng)建一個(gè)待執(zhí)行的變動(dòng)哆键。調(diào)用此方法后訪問 this.state 有可能會(huì)得到當(dāng)前已存在的 state(譯注:指 state 尚未來得及改變)。
即使用 setState() 的第二種形式:以一個(gè)函數(shù)而不是對(duì)象作為參數(shù)瘦锹,此函數(shù)的第一個(gè)參數(shù)是前一刻的state籍嘹,第二個(gè)參數(shù)是 state 更新執(zhí)行瞬間的 props。
// 正確用法
this.setState((prevState, props) => ({
? ? count: prevState.count + props.increment
}))
這種函數(shù)式 setState() 工作機(jī)制類似:
[
? ? {increment: 1},
? ? {increment: 1},
? ? {increment: 1}
].reduce((prevState, props) => ({
? ? count: prevState.count + props.increment
}), {count: 0})
// {count: 3}
關(guān)鍵點(diǎn)在于更新函數(shù)(updater function):
(prevState, props) => ({
? count: prevState.count + props.increment
})
這基本上就是個(gè) reducer弯院,其中 prevState 類似于一個(gè)累加器(accumulator)辱士,而 props 則像是新的數(shù)據(jù)源。類似于 Redux 中的 reducers听绳,你可以使用任何標(biāo)準(zhǔn)的 reduce 工具庫對(duì)該函數(shù)進(jìn)行 reduce(包括 Array.prototype.reduce())颂碘。同樣類似于 Redux,reducer 應(yīng)該是 純函數(shù) 椅挣。
注意:企圖直接修改 prevState 通常都是初學(xué)者困惑的根源头岔。
相關(guān)源碼:
// 將新的 state 合并到狀態(tài)隊(duì)列
var nextState = this._processPendingState(nextProps, nextContext)
// 根據(jù)更新隊(duì)列和 shouldComponentUpdate 的狀態(tài)來判斷是否需要更新組件
var shouldUpdate = this._pendingForceUpdate ||
? ? !inst.shouldComponentUpdate ||
? ? inst.shouldComponentUpdate(nextProps, nextState, nextContext)
二、setState 循環(huán)調(diào)用風(fēng)險(xiǎn)
當(dāng)調(diào)用 setState 時(shí)鼠证,實(shí)際上是會(huì)執(zhí)行 enqueueSetState 方法峡竣,并會(huì)對(duì) partialState 及 _pendingStateQueue 隊(duì)列進(jìn)行合并操作,最終通過 enqueueUpdate 執(zhí)行 state 更新名惩。
而 performUpdateIfNecessary 獲取 _pendingElement澎胡、_pendingStateQueue孕荠、_pendingForceUpdate娩鹉,并調(diào)用 reaciveComponent 和 updateComponent 來進(jìn)行組件更新。
**但稚伍,如果在 shouldComponentUpdate 或 componentWillUpdate 方法里調(diào)用 this.setState 方法弯予,就會(huì)造成崩潰。**這是因?yàn)樵?shouldComponentUpdate 或 componentWillUpdate 方法里調(diào)用 this.setState 時(shí)个曙,this._pendingStateQueue!=null锈嫩,則 performUpdateIfNecessary 方法就會(huì)調(diào)用 updateComponent 方法進(jìn)行組件更新,而 updateComponent 方法又會(huì)調(diào)用 shouldComponentUpdate和componentWillUpdate 方法垦搬,因此造成循環(huán)調(diào)用呼寸,使得瀏覽器內(nèi)存占滿后崩潰。
圖 2-1 循環(huán)調(diào)用
setState 源碼:
// 更新 state
ReactComponent.prototype.setState = function(partialState, callback) {
? ? this.updater.enqueueSetState(this, partialState)
? ? if (callback) {
? ? ? ? this.updater.enqueueCallback(this, callback, 'setState')
? ? }
}
enqueueSetState: function(publicInstance, partialState) {
? ? var internalInstance = getInternalInstanceReadyForUpdate(
? ? ? ? publicInstance,
? ? ? ? 'setState'
? ? )
? ? if (!internalInstance) {
? ? ? ? return
? ? }
? ? // 更新隊(duì)列合并操作
? ? var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue=[])
? ? queue.push(partialState)
? ? enqueueUpdate(internalInstance)
}
// 如果存在 _pendingElement猴贰、_pendingStateQueue对雪、_pendingForceUpdate,則更新組件
performUpdateIfNecessary: function(transaction) {
? ? if (this._pendingElement != null) {
? ? ? ? ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context)
? ? }
? ? if (this._pendingStateQueue != null || this._pendingForceUpdate) {
? ? ? ? this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context)
? ? }
}
三米绕、setState 調(diào)用棧
既然 setState 是通過 enqueueUpdate 來執(zhí)行 state 更新的瑟捣,那 enqueueUpdate 是如何實(shí)現(xiàn)更新 state 的喃馋艺?
圖3-1 setState 簡(jiǎn)化調(diào)用棧
上面這個(gè)流程圖是一個(gè)簡(jiǎn)化的 setState 調(diào)用棧,注意其中核心的狀態(tài)判斷迈套,在源碼(ReactUpdates.js)中
function enqueueUpdate(component) {
? // ...
? if (!batchingStrategy.isBatchingUpdates) {
? ? batchingStrategy.batchedUpdates(enqueueUpdate, component);
? ? return;
? }
? dirtyComponents.push(component);
}
若 isBatchingUpdates 為 false 時(shí)捐祠,所有隊(duì)列中更新執(zhí)行 batchUpdate,否則桑李,把當(dāng)前組件(即調(diào)用了 setState 的組件)放入 dirtyComponents 數(shù)組中踱蛀。先不管這個(gè) batchingStrategy,看到這里大家應(yīng)該已經(jīng)大概猜出來了贵白,文章一開始的例子中 4 次 setState 調(diào)用表現(xiàn)之所以不同星岗,這里邏輯判斷起了關(guān)鍵作用。
那么 batchingStrategy 究竟是何方神圣呢戒洼?其實(shí)它只是一個(gè)簡(jiǎn)單的對(duì)象俏橘,定義了一個(gè) isBatchingUpdates 的布爾值,和一個(gè) batchedUpdates 方法圈浇。下面是一段簡(jiǎn)化的定義代碼:
var batchingStrategy = {
? isBatchingUpdates: false,
? batchedUpdates: function(callback, a, b, c, d, e) {
? ? // ...
? ? batchingStrategy.isBatchingUpdates = true;
? ? transaction.perform(callback, null, a, b, c, d, e);
? }
};
注意 batchingStrategy 中的 batchedUpdates 方法中寥掐,有一個(gè) transaction.perform 調(diào)用。這就引出了本文要介紹的核心概念 —— Transaction(事務(wù))磷蜀。
四召耘、初識(shí)事物
在 Transaction 的源碼中有一幅特別的 ASCII 圖,形象的解釋了 Transaction 的作用褐隆。
/*
* <pre>
*? ? ? ? ? ? ? ? ? ? ? wrappers (injected at creation time)
*? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? +? ? ? ? +
*? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? |
*? ? ? ? ? ? ? ? ? ? +-----------------|--------|--------------+
*? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? v? ? ? ? |? ? ? ? ? ? ? |
*? ? ? ? ? ? ? ? ? ? |? ? ? +---------------+? |? ? ? ? ? ? ? |
*? ? ? ? ? ? ? ? ? ? |? +--|? ? wrapper1? |---|----+? ? ? ? |
*? ? ? ? ? ? ? ? ? ? |? |? +---------------+? v? ? |? ? ? ? |
*? ? ? ? ? ? ? ? ? ? |? |? ? ? ? ? +-------------+? |? ? ? ? |
*? ? ? ? ? ? ? ? ? ? |? |? ? +----|? wrapper2? |--------+? |
*? ? ? ? ? ? ? ? ? ? |? |? ? |? ? +-------------+? |? ? |? |
*? ? ? ? ? ? ? ? ? ? |? |? ? |? ? ? ? ? ? ? ? ? ? |? ? |? |
*? ? ? ? ? ? ? ? ? ? |? v? ? v? ? ? ? ? ? ? ? ? ? v? ? v? | wrapper
*? ? ? ? ? ? ? ? ? ? | +---+ +---+? +---------+? +---+ +---+ | invariants
* perform(anyMethod) | |? | |? |? |? ? ? ? |? |? | |? | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
*? ? ? ? ? ? ? ? ? ? | |? | |? |? |? ? ? ? |? |? | |? | |
*? ? ? ? ? ? ? ? ? ? | |? | |? |? |? ? ? ? |? |? | |? | |
*? ? ? ? ? ? ? ? ? ? | |? | |? |? |? ? ? ? |? |? | |? | |
*? ? ? ? ? ? ? ? ? ? | +---+ +---+? +---------+? +---+ +---+ |
*? ? ? ? ? ? ? ? ? ? |? initialize? ? ? ? ? ? ? ? ? ? close? ? |
*? ? ? ? ? ? ? ? ? ? +-----------------------------------------+
* </pre>
*/
簡(jiǎn)單地說污它,一個(gè)所謂的 Transaction 就是將需要執(zhí)行的 method 使用 wrapper 封裝起來,再通過 Transaction 提供的 perform 方法執(zhí)行庶弃。而在 perform 之前衫贬,先執(zhí)行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 執(zhí)行后)再執(zhí)行所有的 close 方法歇攻。一組 initialize 及 close 方法稱為一個(gè) wrapper固惯,從上面的示例圖中可以看出 Transaction 支持多個(gè) wrapper 疊加。
具體到實(shí)現(xiàn)上缴守,React 中的 Transaction 提供了一個(gè) Mixin 方便其它模塊實(shí)現(xiàn)自己需要的事務(wù)葬毫。而要使用 Transaction 的模塊,除了需要把 Transaction 的 Mixin 混入自己的事務(wù)實(shí)現(xiàn)中外屡穗,還需要額外實(shí)現(xiàn)一個(gè)抽象的 getTransactionWrappers 接口贴捡。這個(gè)接口是 Transaction 用來獲取所有需要封裝的前置方法(initialize)和收尾方法(close)的,因此它需要返回一個(gè)數(shù)組的對(duì)象村砂,每個(gè)對(duì)象分別有 key 為 initialize 和 close 的方法烂斋。
下面是一個(gè)簡(jiǎn)單使用 Transaction 的例子
var Transaction = require('./Transaction');
// 我們自己定義的 Transaction
var MyTransaction = function() {
? // do sth.
};
Object.assign(MyTransaction.prototype, Transaction.Mixin, {
? getTransactionWrappers: function() {
? ? return [{
? ? ? initialize: function() {
? ? ? ? console.log('before method perform');
? ? ? },
? ? ? close: function() {
? ? ? ? console.log('after method perform');
? ? ? }
? ? }];
? };
});
var transaction = new MyTransaction();
var testMethod = function() {
? console.log('test');
}
transaction.perform(testMethod);
// before method perform
// test
// after method perform
當(dāng)然在實(shí)際代碼中 React 還做了異常處理等工作,這里不詳細(xì)展開。有興趣的同學(xué)可以參考源碼中 Transaction 實(shí)現(xiàn)源祈。
說了這么多 Transaction煎源,它到底是怎么導(dǎo)致上文所述 setState 的各種不同表現(xiàn)的呢?
五香缺、解密 setState
那么 Transaction 跟 setState 的不同表現(xiàn)有什么關(guān)系呢手销?首先我們把 4 次 setState 簡(jiǎn)單歸類,前兩次屬于一類图张,因?yàn)樗麄冊(cè)谕淮握{(diào)用棧中執(zhí)行锋拖;setTimeout 中的兩次 setState 屬于另一類,原因同上祸轮。讓我們分別看看這兩類 setState 的調(diào)用棧:
圖 5-1 componentDidMount 里的 setState 調(diào)用棧
圖 5-2 setTimeout 里的 setState 調(diào)用棧
很明顯兽埃,在 componentDidMount 中直接調(diào)用的兩次 setState,其調(diào)用棧更加復(fù)雜适袜;而 setTimeout 中調(diào)用的兩次 setState柄错,調(diào)用棧則簡(jiǎn)單很多。讓我們重點(diǎn)看看第一類 setState 的調(diào)用棧苦酱,有沒有發(fā)現(xiàn)什么熟悉的身影售貌?沒錯(cuò),就是batchedUpdates 方法疫萤,原來早在 setState 調(diào)用前颂跨,已經(jīng)處于 batchedUpdates 執(zhí)行的 transaction 中!
那這次 batchedUpdate 方法扯饶,又是誰調(diào)用的呢恒削?讓我們往前再追溯一層,原來是 ReactMount.js 中的**_renderNewRootComponent** 方法尾序。也就是說钓丰,整個(gè)將 React 組件渲染到 DOM 中的過程就處于一個(gè)大的 Transaction 中。
六蹲诀、回到題目
接下來的解釋就順理成章了斑粱,因?yàn)樵?componentDidMount 中調(diào)用 setState 時(shí)弃揽,batchingStrategy 的 isBatchingUpdates 已經(jīng)被設(shè)為 true脯爪,所以兩次 setState 的結(jié)果并沒有立即生效,而是被放進(jìn)了 dirtyComponents 中矿微。這也解釋了兩次打印this.state.val 都是 0 的原因痕慢,新的 state 還沒有被應(yīng)用到組件中。
再反觀 setTimeout 中的兩次 setState涌矢,因?yàn)闆]有前置的 batchedUpdate 調(diào)用掖举,所以 batchingStrategy 的 isBatchingUpdates 標(biāo)志位是 false,也就導(dǎo)致了新的 state 馬上生效娜庇,沒有走到 dirtyComponents 分支塔次。也就是方篮,setTimeout 中第一次 setState 時(shí),this.state.val 為 1励负,而 setState 完成后打印時(shí) this.state.val 變成了 2藕溅。第二次 setState 同理。
在上文介紹 Transaction 時(shí)也提到了其在 React 源碼中的多處應(yīng)用继榆,想必調(diào)試過 React 源碼的同學(xué)應(yīng)該能經(jīng)常見到它的身影巾表,像 initialize、perform略吨、close集币、closeAll、notifyAll 等方法出現(xiàn)在調(diào)用棧里時(shí)翠忠,都說明當(dāng)前處于一個(gè) Transaction 中鞠苟。
既然事務(wù)那么有用,那我們可以用它嗎秽之?
答案是不能偶妖,但在 React 15.0 之前的版本中還是為開發(fā)者提供了 batchedUpdates 方法,它可以解決針對(duì)一開始例子中 setTimeout 里的兩次 setState 導(dǎo)致 rendor 的情況:
import ReactDom, { unstable_batchedUpdates } from 'react-dom';
unstable_batchedUpdates(() => {
? this.setState(val: this.state.val + 1);
? this.setState(val: this.state.val + 1);
});
在 React 15.0 之后的版本已經(jīng)將 batchedUpdates 徹底移除了政溃,所以趾访,不再建議使用。
本文是《深入React技術(shù)椂》解密setState讀書筆記以及自己的一些補(bǔ)充理解