從 setState promise 化的探討 體會(huì) React 團(tuán)隊(duì)設(shè)計(jì)思想

Tomorrow Land 2017 - Martin Garrix

從 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 文章:

Happy Coding!

PS:
作者Github倉庫知乎問答鏈接
歡迎各種形式交流味咳。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末庇勃,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子槽驶,更是在濱河造成了極大的恐慌责嚷,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掂铐,死亡現(xiàn)場離奇詭異罕拂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)全陨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門爆班,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人辱姨,你說我怎么就攤上這事柿菩。” “怎么了雨涛?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵枢舶,是天一觀的道長。 經(jīng)常有香客問我替久,道長凉泄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任侣肄,我火速辦了婚禮旧困,結(jié)果婚禮上醇份,老公的妹妹穿的比我還像新娘稼锅。我一直安慰自己,他們只是感情好僚纷,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布矩距。 她就那樣靜靜地躺著,像睡著了一般怖竭。 火紅的嫁衣襯著肌膚如雪锥债。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音哮肚,去河邊找鬼登夫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛允趟,可吹牛的內(nèi)容都是我干的恼策。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼潮剪,長吁一口氣:“原來是場噩夢啊……” “哼涣楷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起抗碰,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤狮斗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后弧蝇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碳褒,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年看疗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了骤视。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鹃觉,死狀恐怖专酗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情盗扇,我是刑警寧澤祷肯,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站疗隶,受9級(jí)特大地震影響佑笋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜斑鼻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一蒋纬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坚弱,春花似錦蜀备、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至些楣,卻和暖如春脂凶,著一層夾襖步出監(jiān)牢的瞬間宪睹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國打工蚕钦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留亭病,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓嘶居,卻偏偏與公主長得像命贴,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子食听,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容