從 setState 那個(gè)眾所周知的小秘密說起...
在 React 組件中漠其,調(diào)用 this.setState() 是最基本的場景目尖。這個(gè)方法描述了 state 的變化园骆、觸發(fā)了組件 re-rendering畏线。但是擎厢,也許看似平常的 this.setState() 里面卻也許蘊(yùn)含了很多鮮為人知的設(shè)計(jì)和討論朵夏。
相信很多開發(fā)者已經(jīng)意識(shí)到付秕,setState 方法“或許”是異步的。也許你覺得侍郭,看上去更新 state 是如此輕而易舉的操作询吴,這并沒有什么可異步處理的掠河。但是要意識(shí)到,因?yàn)?state 的更新會(huì)觸發(fā) re-rendering猛计,而 re-rendering 代價(jià)昂貴唠摹,短時(shí)間內(nèi)反復(fù)進(jìn)行渲染在性能上肯定是不可取的。所以奉瘤,React 采用 batching 思想勾拉,它會(huì) batches 一系列連續(xù)的 state 更新,而只觸發(fā)一次 re-render盗温。
關(guān)于這些內(nèi)容藕赞,如果你還不清楚,推薦參考@程墨的系列文章:setState:這個(gè)API設(shè)計(jì)到底怎么樣卖局;英語好的話斧蜕,可以直接關(guān)注長發(fā)飄飄的 Eric Elliott 著名的引起系列口水戰(zhàn)的吐槽文:setState() Gate。
或者砚偶,直接看下面的一個(gè)小例子批销。
比如,最簡單的一個(gè)場景是:
function incrementMultiple() {
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
}
直觀上來看染坯,當(dāng)上面的 incrementMultiple 函數(shù)被調(diào)用時(shí)均芽,組件狀態(tài)的
count 值被增加了3次,每次增加1单鹿,那最后 count 被增加了3掀宋。但是,實(shí)際上的結(jié)果只給 state 增加了1仲锄。不信你自己試試~
讓 setState 連續(xù)更新的幾個(gè) hack
如果想讓 count 一次性加3布朦,應(yīng)該如何優(yōu)雅地處理潛在的異步操作,規(guī)避上述問題呢昼窗?
以下提供幾種解決方案:
方法一:常見的一種做法便是將一個(gè)回調(diào)函數(shù)傳入 setState 方法中是趴。即 setState 著名的函數(shù)式用法。這樣能保證即便在更新被 batched 時(shí)澄惊,也能訪問到預(yù)期的 state 或 props唆途。(后面會(huì)解釋這么做的原理)
-
方法二:另外一個(gè)常見的做法是需要在 setState 更新之后進(jìn)行的邏輯(比如上述的連續(xù)第二次 count + 1),封裝到一個(gè)函數(shù)中掸驱,并作為第二個(gè)參數(shù)傳給 setState肛搬。這段函數(shù)邏輯將會(huì)在更新后由 React 代理執(zhí)行。即:
setState(updater, [callback])
方法三:把需要在 setState 更新之后進(jìn)行的邏輯放在一個(gè)合適的生命周期 hook 函數(shù)中毕贼,比如 componentDidMount 或者 componentDidUpdate 也當(dāng)然可以解決問題温赔。也就是說 count 第一次 +1 之后,出發(fā) componentDidUpdate 生命周期 hook鬼癣,第二次 count +1 操作直接放在 componentDidUpdate 函數(shù)里面就好啦陶贼。
一個(gè)引起廣泛討論的 Issue
這些內(nèi)容貌似已經(jīng)不再新鮮啤贩,很多 React 資深開發(fā)者其實(shí)都是了解的,或能很快理解拜秧。
可是痹屹,你想過這個(gè)問題嗎:
現(xiàn)代 javascript 處理異步流程,很流行的一個(gè)做法是使用 promises枉氮,那么我們能否應(yīng)用這個(gè)思路解決呢志衍?
說具體一些,就是調(diào)用 setState 方法之后聊替,返回一個(gè) promise楼肪,狀態(tài)更新完畢后我們?cè)谡{(diào)用 promise.then 進(jìn)行下一步處理。
答案是肯定的惹悄,但是卻被官方否決了春叫。
我是如何得出“答案是肯定的,但是是不被官方建議的俘侠。”這個(gè)結(jié)論蔬将,喜歡刨根問底的讀者請(qǐng)繼續(xù)往下閱讀爷速,相信你一定會(huì)有所啟發(fā),也能更充分理解 React 團(tuán)隊(duì)的設(shè)計(jì)思想霞怀。
第 2642 Issue 解讀和深入分析
我是一步一步在 Facebook 開源 React 的官方 Github倉庫上惫东,找到了線索。
整個(gè)過程跟下來毙石,相信在各路大神的 comments 之間廉沮,你會(huì)對(duì) React 的設(shè)計(jì)理念以及 javascript 解決問題的思路有一個(gè)更清晰的認(rèn)識(shí)。
一切的探究始于 React 第 #2642 號(hào) issue: Make setState return a promise徐矩,上面關(guān)于 count 連續(xù) +3 大家已經(jīng)有所了解滞时。接下來我舉一個(gè)真正在生產(chǎn)開發(fā)中的例子,方便大家理解討論滤灯。
我們現(xiàn)在開發(fā)一個(gè)可編輯的 table坪稽,需求是:當(dāng)用戶敲下“回車”,光標(biāo)將會(huì)進(jìn)入下一行(調(diào)用 setState 進(jìn)行光標(biāo)移動(dòng))鳞骤;如果用戶當(dāng)前已經(jīng)在最后一行窒百,那么敲下回車時(shí),第一步將先創(chuàng)建一個(gè)新行(調(diào)用 setState 創(chuàng)建新的最后一行)豫尽,在新行創(chuàng)建之后篙梢,再去新的最后一行進(jìn)行光標(biāo)聚焦(調(diào)用 setState 進(jìn)行光標(biāo)移動(dòng))。
常見且錯(cuò)誤的處理在于:
this.setState({
selected: input
// 創(chuàng)建新行
}.bind(this));
this.props.didSelect(this.state.selected);
因?yàn)榈谝粋€(gè) this.setState 是異步進(jìn)行的話美旧,下一處 didSelect 方法執(zhí)行 this.setState 時(shí)渤滞,所處理的參數(shù) this.state.selected 可能還不是預(yù)期的下一行贬墩。很明顯,這就是 this.setState 的異步性帶來的問題蔼水。
為了解決這個(gè)完成這樣的邏輯震糖,想到了 setState 第二個(gè)參數(shù)解決方案,用代碼簡單表述就是:
this.setState({
selected: input
// 創(chuàng)建新行
}, function() {
this.props.didSelect(this.state.selected);
}).bind(this));
這種解決方案是使用嵌套的 setState 方法趴腋。但這無疑潛在地會(huì)帶來嵌套地獄的問題吊说。
Promise 化方案登場
這一切是不是像極了傳統(tǒng) Javascript 處理異步老套路?解決回調(diào)地獄优炬,你是不是應(yīng)激性地想到了 promise颁井?
如果 setState 方法返回的是一個(gè) promises,自然會(huì)更加優(yōu)雅:
setState() currently accepts an optional second argument for callback and returns undefined.
This results in a callback hell for a very stateful component. Having it return a promise would make it much more managable.
如果用 promise 風(fēng)格解決問題的話蠢护,無非就是:
this.setState({
selected: input
}).then(function() {
this.props.didSelect(this.state.selected);
}.bind(this));
看上去沒什么問題雅宾,一個(gè)很時(shí)髦的設(shè)計(jì)。但是葵硕,我們進(jìn)一步想:如果想讓 React 支持這樣的特性眉抬,采用提出 pull request 的方式,我們?cè)撊绾稳ジ脑创a呢懈凹?
探索 React 源碼蜀变,完成 setState promise 化的改造
首先找到源碼中關(guān)于 setState 定義的地方,它在 react/src/isomorphic/modern/class/ReactBaseClasses.js 這個(gè)目錄下:
ReactComponent.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
我們首先看到一句注釋:
You can provide an optional callback that will be executed when the call to setState is actually completed.
這是采用 setState 第二個(gè)參數(shù)傳入處理回調(diào)的基礎(chǔ)介评。
另外库北,從注釋中我們還找到:
When a function is provided to setState, it will be called at some point in the future (not synchronously). It will be called with the up to date component arguments (state, props, context).
這是給 setState 方法直接傳入一個(gè)函數(shù)的基礎(chǔ)。
言歸正傳们陆,如何改動(dòng)源碼寒瓦,使得 setState promise 化呢?
其實(shí)很簡單坪仇,我直接上代碼:
ReactComponent.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
+ let callbackPromise;
+ if (!callback) {
+ class Deferred {
+ constructor() {
+ this.promise = new Promise((resolve, reject) => {
+ this.reject = reject;
+ this.resolve = resolve;
+ });
+ }
+ }
+ callbackPromise = new Deferred();
+ callback = () => {
+ callbackPromise.resolve();
+ };
+ }
this.updater.enqueueSetState(this, partialState, callback, 'setState');
+
+ if (callbackPromise) {
+ return callbackPromise.promise;
+ }
};
我用 “+” 標(biāo)注了對(duì)源碼所做的更改杂腰。如果開發(fā)者調(diào)用 setState 方法時(shí),傳入的是一個(gè) javascript 對(duì)象的話椅文,那么會(huì)返回一個(gè) promise颈墅,這個(gè) promise 將會(huì)在 state 更新完畢后 resolve。
如果您看不懂的話雾袱,建議補(bǔ)充一下相關(guān)的基礎(chǔ)知識(shí)恤筛,或者留言與我討論。
解決方案有了芹橡,可是 React 官方會(huì)接受這個(gè) PR 嗎毒坛?
很遺憾,答案是否定的。我們來從 React 設(shè)計(jì)思想上煎殷,和 React 官方團(tuán)隊(duì)的回應(yīng)上屯伞,了解一下否決理由。
sebmarkbage(Facebook 工程師豪直,React 核心開發(fā)者)認(rèn)為:解決異步帶來的困擾方案其實(shí)很多劣摇。比如,我們可以在合適的生命周期 hook 函數(shù)中完成相關(guān)邏輯弓乙。在這個(gè)場景里末融,就是在行組件的 componentDidMount 里調(diào)用 focus,自然就完成了自動(dòng)聚焦暇韧。
此外勾习,還有一個(gè)方法:新的 refs 接口設(shè)計(jì)支持接收一個(gè)回調(diào)函數(shù),當(dāng)其子組件掛載時(shí)懈玻,這個(gè)回調(diào)函數(shù)就會(huì)相應(yīng)觸發(fā)巧婶。
所有上述模式都可以完全取代之前的問題方案,即使不能也不意味著要接受 promises 化這個(gè)PR涂乌。
為此艺栈,sebmarkbage 說了一段很扎心的話:
Honestly, the current batching strategy comes with a set of problems right now. I'm hesitant to expand on it's API before we're sure that we're going to keep the current model. I think of it as a temporary escape until we figure out something better.
問題的根源在于現(xiàn)有的 batching 策略,實(shí)話實(shí)說湾盒,這個(gè)策略帶來了一系列問題湿右。也許這個(gè)在后期后有調(diào)整,在 batching 策略是否調(diào)整之前历涝,盲目的擴(kuò)充 setState 接口只會(huì)是一個(gè)短視的行為诅需。
對(duì)此漾唉,Redux 原作者 Dan Abramov 也發(fā)表了自己的看法荧库。他認(rèn)為,以他的經(jīng)驗(yàn)來看赵刑,任何需要使用 setState 第二個(gè)參數(shù) callback 的場景分衫,都可以使用生命周期函數(shù) componentDidUpdate (and/or componentDidMount) 來復(fù)寫。
In my experience, whenever I'm tempted to use setState callback, I can achieve the same by overriding componentDidUpdate (and/or componentDidMount).
另外般此,在一些極端場景下蚪战,如果開發(fā)者確實(shí)需要同步的處理方式,比如如果我想在某 DOM 元素掛載到屏幕之前做一些操作铐懊,promises 這種方案便不可行邀桑。因?yàn)?Promises 總是異步的。反過來科乎,如果 setState 支持這兩種不同的方式壁畸,那么似乎也是完全沒有必要而多余的。
在社區(qū),確實(shí)很多第三方庫漸漸地接受使用 promises 風(fēng)格捏萍,但是這些庫解決的問題往往都是強(qiáng)異步性的太抓,比如文件讀取、網(wǎng)絡(luò)操作等等令杈。 React 似乎沒有必要增加這么一個(gè) confusing 的特性走敌。
另外,如果每個(gè) setState 都返回一個(gè) promises逗噩,也會(huì)帶來性能影響:對(duì)于 React 來說掉丽,setState 將必然產(chǎn)生一個(gè) callback,這些 callbacks 需要合理儲(chǔ)存给赞,以便在合適時(shí)間來觸發(fā)机打。
總結(jié)一下,解決 setState 異步帶來的問題片迅,有很多方式能夠完美優(yōu)雅地解決残邀。在這種情況下,直接讓 setState 返回 promise 是畫蛇添足的柑蛇。另外芥挣,這樣也會(huì)引起性能問題等等。
我個(gè)人認(rèn)為耻台,這樣的思路很好空免,但是難免有些 Overengineering。
這一次為自己瘋狂盆耽,我和我的倔強(qiáng)
怎么樣蹋砚,是否說服你了呢?如果沒有摄杂,在不能更改 React 源碼情況下坝咐,你就是想用 promise 化的 setState,怎么辦呢析恢?
這里提供一個(gè)“反模式”的方案:我們不改變?cè)创a墨坚,自己也可以進(jìn)行改造,原理上就是直接對(duì) this.setState 進(jìn)行攔截映挂,進(jìn)而進(jìn)行 promise 化泽篮,再封裝一個(gè)新的接口出來。
import Promise from "bluebird";
export default {
componentWillMount() {
this.setStateAsync = Promise.promisify(this.setState);
},
};
之后柑船,便可以異步地:
this.setStateAsync({
loading: true,
}).then(this.loadSomething).then((result) => {
return this.setStateAsync({result, loading: false});
});
當(dāng)然帽撑,也可以使用原聲的 promises:
function setStatePromise(that, newState) {
return new Promise((resolve) => {
that.setState(newState, () => {
resolve();
});
});
}
甚至...我們還可以腦洞大開使用 async/await。
最后鞍时,所有這種做法非常的 dirty亏拉,我是不建議這么使用的。
總結(jié)
其實(shí)研究一下 React Issue,深入源碼學(xué)習(xí)专筷,收獲確實(shí)很多弱贼。總結(jié)也沒有更多想說的了磷蛹,無恥滴做個(gè)廣告吧:
我的其他關(guān)于 React 文章:
解析Twitter前端架構(gòu) 學(xué)習(xí)復(fù)雜場景數(shù)據(jù)設(shè)計(jì)
React+Redux打造“NEWS EARLY”單頁應(yīng)用 一個(gè)項(xiàng)目理解最前沿技術(shù)棧真諦
Happy Coding!