摘要:我們來從設(shè)計(jì)思想上,和官方團(tuán)隊(duì)的回應(yīng)上,了解一下否決理由。此外,還有一個(gè)方法新的接口設(shè)計(jì)支持接收一個(gè)回調(diào)函數(shù),當(dāng)其子組件掛載時(shí),這個(gè)回調(diào)函數(shù)就會(huì)相應(yīng)觸發(fā)。
從 setState 那個(gè)眾所周知的小秘密說起...
在 React 組件中,調(diào)用 this.setState() 是最基本的場(chǎng)景。這個(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ì)到底怎么樣;英語(yǔ)好的話,可以直接關(guān)注長(zhǎng)發(fā)飄飄的 Eric Elliott 著名的引起系列口水戰(zhàn)的吐槽文:setState() Gate。
或者,直接看下面的一個(gè)小例子。
比如,最簡(jiǎn)單的一個(gè)場(chǎng)景是:
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。不信你自己試試~
如果想讓 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倉(cāng)庫(kù)上,找到了線索。
整個(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ù)解決方案,用代碼簡(jiǎn)單表述就是:
this.setState({ selected: input // 創(chuàng)建新行 }, function() { this.props.didSelect(this.state.selected); }).bind(this));
這種解決方案是使用嵌套的 setState 方法。但這無(wú)疑潛在地會(huì)帶來嵌套地獄的問題。
Promise 化方案登場(chǎng)這一切是不是像極了傳統(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)格解決問題的話,無(wú)非就是:
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í)很簡(jiǎn)單,我直接上代碼:
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 設(shè)計(jì)思想上,和 React 官方團(tuán)隊(duì)的回應(yīng)上,了解一下否決理由。
sebmarkbage(Facebook 工程師,React 核心開發(fā)者)認(rèn)為:解決異步帶來的困擾方案其實(shí)很多。比如,我們可以在合適的生命周期 hook 函數(shù)中完成相關(guān)邏輯。在這個(gè)場(chǎng)景里,就是在行組件的 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 的場(chǎng)景,都可以使用生命周期函數(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).
另外,在一些極端場(chǎng)景下,如果開發(fā)者確實(shí)需要同步的處理方式,比如如果我想在某 DOM 元素掛載到屏幕之前做一些操作,promises 這種方案便不可行。因?yàn)?Promises 總是異步的。反過來,如果 setState 支持這兩種不同的方式,那么似乎也是完全沒有必要而多余的。
在社區(qū),確實(shí)很多第三方庫(kù)漸漸地接受使用 promises 風(fēng)格,但是這些庫(kù)解決的問題往往都是強(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é)也沒有更多想說的了,無(wú)恥滴做個(gè)廣告吧:
我的其他關(guān)于 React 文章:
React Redux 中間件思想遇見 Web Worker 的靈感(附demo)
通過實(shí)例,學(xué)習(xí)編寫 React 組件的“最佳實(shí)踐”
React 組件設(shè)計(jì)和分解思考
[從 React 綁定 this,看 JS 語(yǔ)言發(fā)展和框架設(shè)計(jì)]()
React 服務(wù)端渲染如此輕松 從零開始構(gòu)建前后端應(yīng)用
做出Uber移動(dòng)網(wǎng)頁(yè)版還不夠 極致性能打造才見真章
解析Twitter前端架構(gòu) 學(xué)習(xí)復(fù)雜場(chǎng)景數(shù)據(jù)設(shè)計(jì)
React Conf 2017 干貨總結(jié)1: React + ES next = ?
React+Redux打造“NEWS EARLY”單頁(yè)應(yīng)用 一個(gè)項(xiàng)目理解最前沿技術(shù)棧真諦
一個(gè)react+redux工程實(shí)例
Happy Coding!
PS:
作者Github倉(cāng)庫(kù) 和 知乎問答鏈接
歡迎各種形式交流。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/85085.html
摘要:我們來從設(shè)計(jì)思想上,和官方團(tuán)隊(duì)的回應(yīng)上,了解一下否決理由。此外,還有一個(gè)方法新的接口設(shè)計(jì)支持接收一個(gè)回調(diào)函數(shù),當(dāng)其子組件掛載時(shí),這個(gè)回調(diào)函數(shù)就會(huì)相應(yīng)觸發(fā)。 從 setState 那個(gè)眾所周知的小秘密說起... 在 React 組件中,調(diào)用 this.setState() 是最基本的場(chǎng)景。這個(gè)方法描述了 state 的變化、觸發(fā)了組件 re-rendering。但是,也許看似平常的 th...
摘要:右側(cè)展現(xiàn)對(duì)應(yīng)產(chǎn)品。我們使用命名為的對(duì)象表示過濾條件信息,如下此數(shù)據(jù)需要在組件中進(jìn)行維護(hù)。因?yàn)榻M件的子組件和都將依賴這項(xiàng)數(shù)據(jù)狀態(tài)。化應(yīng)用再回到之前的場(chǎng)景,我們?cè)O(shè)計(jì)化函數(shù),進(jìn)一步可以簡(jiǎn)化為對(duì)于的偏應(yīng)用即上面提到的相信大家已經(jīng)理解了這么做的好處。 showImg(https://segmentfault.com/img/remote/1460000014458612?w=1240&h=663...
摘要:右側(cè)展現(xiàn)對(duì)應(yīng)產(chǎn)品。我們使用命名為的對(duì)象表示過濾條件信息,如下此數(shù)據(jù)需要在組件中進(jìn)行維護(hù)。因?yàn)榻M件的子組件和都將依賴這項(xiàng)數(shù)據(jù)狀態(tài)。化應(yīng)用再回到之前的場(chǎng)景,我們?cè)O(shè)計(jì)化函數(shù),進(jìn)一步可以簡(jiǎn)化為對(duì)于的偏應(yīng)用即上面提到的相信大家已經(jīng)理解了這么做的好處。 showImg(https://segmentfault.com/img/remote/1460000014458612?w=1240&h=663...
摘要:寫的姿勢(shì)前兩天去帝都參加第三屆前端開發(fā)者大會(huì),見了很多老朋友,也認(rèn)識(shí)了很多新朋友。 推薦 1. 組件化設(shè)計(jì)思維 – 從規(guī)范到工具的構(gòu)建與探索 http://www.zcool.com.cn/artic... 阿里巴巴在中臺(tái)戰(zhàn)略的背景下,設(shè)計(jì)提效又再次推動(dòng)著設(shè)計(jì)思維的變革。設(shè)計(jì)師們不僅僅需要出色地完成業(yè)務(wù)需求的設(shè)計(jì),同時(shí)還需要思考設(shè)計(jì)的價(jià)值,也就是經(jīng)常提到的最佳方案性價(jià)比。我們需要在設(shè)計(jì)...
閱讀 2345·2023-04-25 14:29
閱讀 1477·2021-11-22 09:34
閱讀 2715·2021-11-22 09:34
閱讀 3398·2021-11-11 10:59
閱讀 1864·2021-09-26 09:46
閱讀 2238·2021-09-22 16:03
閱讀 1929·2019-08-30 12:56
閱讀 486·2019-08-30 11:12