[譯] setState() 門事件

React setState() 解惑

譯註:本文原由於做者的一條推特,他認爲應該避免使用 setState(),隨後引起論戰,遂寫此文詳細闡明其觀點。譯者我的認爲,本文主要在於「撕逼「,並未深刻介紹 setState() 的技術細節,但願從技術層面深刻了解 setState() 的同窗能夠參考[譯] React 將來之函數式 setState。對 setState() 不瞭解的同窗可能會感到本文不知所云,特此說明。javascript

一切都源於上週。3 位 React 初學者嘗試在項目中使用 setState() 時遇到了 3 種不一樣的問題。我指導過不少 React 新手,也爲團隊提供從其餘技術到 React 的架構轉型諮詢。html

其中一位初學者正在開發一個十分適合使用 Redux 的生產項目,因此我沒有正面去解決 setState() 的同步問題(the timing with setState()),而是直接建議他用 Redux 替換掉 setState(),由於使用 Redux 能避免 state 在組件渲染的過程當中發生改變。Redux 簡單地利用來自 store 的 props 來決定如何渲染界面,巧妙地規避了複雜的同步問題。java

所以也就有了下面這條推特:react

「React 有個 setState() 問題:讓新手使用 setState() 毫無好處(a recipe for headaches)。高手們已經學會了如何避免使用它"git

以後,有些高手就來糾正我了:github

「我是 React 團隊的一員。在嘗試其餘方法以前,請學會使用 setState。」編程

「那些所謂‘高手’們怕是要落伍了,由於 React 17 將會默認採用異步調度。」redux

對於第二點:vim

「Fiber 有一種用於暫停、切分、重建和取消更新的策略,但若是你脫離了組件 state,那此策略便沒法正常工做了。」c#

貌似都沒錯,但是碼農們就要罵娘了:

面對困境「呵呵」兩下並沒有妨,不過千萬別呵呵事後就對問題視而不見了。

在和另外一個初學者交流的時候,我發現他也對 setState() 的工做機制感到困惑。他後來索性放棄了,他把 state 塞在一個閉包裏;顯而易見,閉包中 state 的改變是不會觸發 render 函數自動執行的。

考慮到深感困惑的初學者之多,我仍是堅持我上述推文中前半句的觀點;但若是能夠重來的話,我會對後半句稍做修改,由於確有不少高手在(主要是 Facebook 和 Netfix 的工程師)大量地使用 setState()

「React 有個 setState() 問題:叫新手使用 setState() 毫無好處,但高手們自有神技。「

固然,推特仍是有可能會喪失其集體智慧(lose its collective mind)(譯註:我的認爲這句應該是指當網絡上大多數人持某一觀點時,那即便該觀點是錯的,那你也不能指出其錯誤,不然就會招致集體攻訐;或者說,真理有時候只掌握在少數人手裏)。 畢竟,React 是「完美的」, 咱們都必須認可 setState的美妙優雅是多麼的恰如其分,不然只會遭到冷嘲熱諷。

若是 setState() 令你感到困惑,那都是你的問題 —— 你要麼是瘋子,要麼是傻瓜。(我好像忘了說 Javascript 社區的霸凌問題了

好了,當你嘲笑全部初學者的時候,先檢討檢討本身吧,別覺得掌握了 setState() 就能夠忘乎所以了。

那種行爲是荒謬好笑的,是精英主義論的,會讓新手們感到十分討厭。若是人們常常對某個 API 感到困惑的話,那就該改進 API 自己的設計了,或者至少應該改進下文檔。

讓咱們的社區和工具變得更加友好對全部人來講都是件好事。

setState() 究竟有何問題?

這個問題能夠有兩個答案:

  1. 沒啥問題。(大部分狀況下)其表現和設計指望同樣,足以解決目標問題。
  2. 學習曲線問題。對新手而言,一些用原生 JS 和直接的 DOM 操做能夠輕鬆實現的效果,用 React 和 setState 實現起來就會困難重重。

React 的設計初衷本是簡化應用開發流程,可是:

  • 你卻不能爲所欲爲地操做 DOM。
  • 你不能爲所欲爲地(於任什麼時候間、依賴任意數據源)更新 state。
  • 在組件的生命週期中,你並不老是能在屏幕上直接觀察到渲染後的 DOM 元素,這限制了 setState() 的使用時機和方式(由於你有些 state 可能尚未渲染到屏幕上)。

在這幾種狀況下,困惑都來源於 React 組件生命週期的限制性(這些限制是刻意設計的,是好的)。

從屬 State(Dependent State)

更新 state 時,更新結果可能依賴於:

  • 當前 state
  • 同一批次中先前的更新操做
  • 當前已渲染的 DOM (例如:組件的座標位置、可見性、CSS 計算值等等)

當存在這幾種從屬 state 的時候,若是你還想簡單直接地更新 state,那 React 的表現行爲會讓你大吃一驚,而且是以一種使人憎惡又難以調試的方式。大多數狀況下,你的代碼根本沒法工做:要麼 state 不對,要麼控制檯有錯誤。

我之因此吐槽 setState(),是由於它的這種限制性在 API 文檔中並無詳細說明,關於應對這種限制性的各類通用模式也未能闡述清楚。這迫使初學者只能不斷試錯、Google 或者從其餘社區成員那裏尋求幫助,但實際上在文檔中本該就有更好的新手指南。

當前關於 setState() 的文檔開頭以下:

setState(nextState, callback)複製代碼

將 nextState 淺合併到當前 state。這是在事件處理函數和服務器請求回調函數中觸發 UI 更新的主要方法。

在末尾確實也提到了其異步行爲:

不保證 setState 調用會同步執行,考慮到性能問題,可能會對屢次調用做批處理。

這就是不少用戶層(userland) bug 的根本緣由:

// 假設 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複製代碼

本質上等同於:

Object.assign(state,
  {count: state.count + 1},
  {count: state.count + 1},
  {count: state.count + 1}
); // {count: 1}複製代碼

這在文檔中並未顯式說明(在另一份特殊指南中提到了)。

文檔還提到了另一種函數式的 setState() 語法:

也能夠傳遞一個簽名爲 function(state, props) => newState 的函數做爲參數。這會將一個原子性的更新操做加入更新隊列,在設置任何值以前,此操做會查詢前一刻的 state 和 props。

...

setState() 並不會當即改變 this.state ,而是會建立一個待執行的變更。調用此方法後訪問 this.state 有可能會獲得當前已存在的 state(譯註:指 state 還沒有來得及改變)。

API 文檔雖提供了些許線索,但未能以一種清晰明瞭的方式闡明初學者常常遇到的怪異表現。開發模式下,儘管 React 的錯誤信息以有效、準確著稱,但當 setState() 的同步問題出現 bug 的時候控制檯卻沒有任何警告。

Jikku Jose

Pier Bover

StackOverflow 上有關 setState() 的問題大都要歸結於組件的生命週期問題。毫無疑問,React 很是流行,所以那些問題都被,也有着各類參差不齊的回答。

那麼,初學者究竟該如何掌握 setState() 呢?

在 React 的文檔中還有一份名爲 「 state 和生命週期」的指南,該指南提供了更多深刻內容:

「…要解決此問題,請使用 setState() 的第二種形式 —— 以一個函數而不是對象做爲參數,此函數的第一個參數是前一刻的 state,第二個參數是 state 更新執行瞬間的 props :」

// 正確用法
this.setState((prevState, props) => ({
  count: prevState.count + props.increment
}));複製代碼

這個函數參數形式(有時被稱爲「函數式 setState()」)的工做機制更像:

[
  {increment: 1},
  {increment: 1},
  {increment: 1}
].reduce((prevState, props) => ({
  count: prevState.count + props.increment
}), {count: 0}); // {count: 3}複製代碼

不明白 reduce 的工做機制? 參見 「Composing Software」「Reduce」 教程。

關鍵點在於更新函數(updater function)

(prevState, props) => ({
  count: prevState.count + props.increment
})複製代碼

這基本上就是個 reducer,其中 prevState 相似於一個累加器(accumulator),而 props 則像是新的數據源。相似於 Redux 中的 reducers,你可使用任何標準的 reduce 工具庫對該函數進行 reduce(包括 Array.prototype.reduce())。一樣相似於 Redux,reducer 應該是 純函數

注意:企圖直接修改 prevState 一般都是初學者困惑的根源。

API 文檔中並未說起更新函數的這些特性和要求,因此,即便少數幸運的初學者碰巧了解到函數式 setState() 能夠實現一些對象字面量形式沒法實現的功能,最終依然可能困惑不解。

僅僅是新手纔有的問題嗎?

直到如今,在處理表單或是 DOM 元素座標位置的時候,我仍是會時不時得掉到坑裏去。當你使用 setState() 的時候,你必須直接面對組件生命週期的相關問題;但當你使用容器組件或是經過 props 來存儲和傳遞 state 的時候,React 則會替你處理同步問題。

不管你有經驗與否 ,處理共享的可變 state 和 state 鎖(state locks)都是很棘手的。經驗豐富之人只不過是能更加快速地定位問題,而後找出一個巧妙的變通方案罷了。

由於初學者從未遇到過這種問題,更不知規避方案,因此是掉坑裏摔得最慘的。

當問題發生時,你固然能夠選擇和 React 鬥個你死我活;不過,你也能夠選擇讓 React 順其天然的工做。這就是我說即便是對初學者而言,Redux 有時 都比 setState 更簡單的緣由。

在併發系統中,state 更新一般按其中一種方式進行:

  • 當其餘程序(或代碼)正在訪問 state 時,禁止 state 的更新(例如 setState())(譯註:即常見的鎖機制)
  • 引入不可變性來消除共享的可變 state,從而實現對 state 的無限制訪問,而且能夠在任什麼時候間建立新 state(例如 Redux)

在我看來(在向不少學生教授過這兩種方法以後),相比於第二種方法,第一種方法更加容易致使錯誤,也更加容易使人困惑。當 state 更新被簡單地阻塞時(在 setState 的例子中,也能夠叫批處理化或延遲執行),解決問題的正確方法並不十分清晰明瞭。

當遇到 setState() 的同步問題時,個人直覺反應實際上是很簡單的:將 state 的管理上移到 Redux(或 MobX) 或容器組件中。基於多方面緣由 ,我本身使用同時也推薦他人使用 Redux,但很顯然,這並非一條放之四海而皆準的建議

Redux 自有其陡峭的學習曲線,但它規避了共享的可變 state 以及 state 更新同步等複雜問題。所以我發現,一旦我教會了學生如何避免可變性,接下來基本就一路順風了。

對於沒有任何函數式編程經驗的新手而言,學習 Redux 遇到的問題可能會比學習 setState() 遇到的更多 —— 可是,Redux 至少有不少其做者親自講授的免費 教程

React 應當向 Redux 學習:有關 React 編程模式和 setState() 踩坑的視頻教程定能讓 React 主頁錦上添花。

在渲染以前決定 State

將 state 管理移到容器組件(或 Redux)中能促使你從另外一個角度思考組件 state 問題,由於這種狀況下,在組件渲染以前,其 state 必須是既定的(由於你必須將其做爲 props 傳下去)。

重要的事情說三遍:

渲染以前,決定 state!

渲染以前,決定 state!

渲染以前,決定 state!

說完三篇以後就能夠獲得一個顯然的推論:在 render() 函數中調用 setState() 是反模式的。

render 函數中計算從屬 state 是 OK 的(好比說, state 中有 firstNamelastName,據此你計算出 fullName,在 render 函數中這樣作徹底是 OK 的),但我仍是傾向於在容器組件中計算出從屬 state ,而後經過 props 將其傳遞給展現組件(presentation components)。

setState() 該怎麼治?

我傾向於廢棄掉對象字面量形式的 setState(),我知道這(表面上看)更加易於理解也更加方便(譯者:「這」指對象字面量形式的 setState()),但它也是坑之所在啊。用腳指頭都能猜到,確定有人這樣寫:

state.count; // 0
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});複製代碼

而後天真就地覺得 {count: 3}。批量化處理後對象的同名 props 被合併掉的狀況幾乎不多是用戶所指望的行爲,反正我是沒見過這種例子。要是真存在這種狀況,那我必須說這跟 React 的實現細節耦合地太緊密了,根本不能做爲有效參考用例。

我也但願 API 文檔中有關 setState() 的章節可以加上「 state 和聲明週期」這一深度指南的連接,這能給那些想要全面學習 setState() 的用戶更多的細節內容。setState() 並不是同步操做,也無任何有意義的返回結果,僅僅是簡單地描述其函數簽名而沒有深刻地探討其各類影響和表現,這對初學者是極不友好的。

初學者必須花上大量時間去找出問題:Google 上搜、StackOverflow 上搜、GitHub issues 裏搜。

setState() 爲什麼如此嚴苛?

setState() 的怪異表現並不是 bug,而是特性。實際上,甚至能夠說這是 React 之因此存在的根本緣由

React 的一大創做動機就是保證肯定性渲染:給定應用 state ,渲染出特定結果。理想狀況下,給定 state 相同,渲染結果也應相同。

爲了達到此目的,當發生變化時,React 經過採起一些限制性手段來管理變化。咱們不能隨意取得某些 DOM 節點而後就地修改之。相反,React 負責 DOM 渲染;當 state 發生改變時,也由React 決定如何重繪。咱們不渲染 DOM,而是由 React 來負責

爲了不在 state 更新的過程當中觸發重繪,React 引入了一條規則:

React 用於渲染的 state 不能在 DOM 渲染的過程當中發生改變。咱們不能決定組件 state 什麼時候獲得更新,而是由 React 來決定

困惑就此而來。當你調用 setState() 時,你覺得你設置了 state ,其實並無。

「你就接着裝逼,你覺得你因此爲的就是你因此爲的嗎?」

什麼時候使用 setState()?

我通常只在不須要持久化 state 的自包含功能單元中使用 setState(),例如可複用的表單校驗組件、自定義的日期或時間選擇部件(widget)、可自定義界面的數據可視化部件等。

我稱這種組件爲「小部件(widget)」,它們通常由兩個或兩個以上組件構成:一個負責內部 state 管理的容器組件,一個或多個負責界面顯示的子組件

幾條立見分曉的檢驗方法(litmus tests):

  • 是否有其餘組件是否依賴於該 state ?
  • 是否須要持久化 state ?(存儲於 local storage 或服務器)

若是這兩個問題的答案都是「否」的話,那使用 setState() 基本是沒問題的;不然,就要另做考慮了。

據我所知,Facebook 使用受管於 Relay containersetState() 來包裝 Facebook UI 的各個不一樣部分,例如大型 Facebook 應用內部的迷你型應用。於 Facebook 而言,以這種方式將複雜的數據依賴和須要實際使用這些數據的組件放在一塊兒是很好的。

對於大型(企業級)應用,我也推薦這種策略。若是你的應用代碼量很是大(十萬行以上),那此策略多是很好的 —— 但這並不意味着這種方式就不能應用於小型應用中。

相似地,並不意味着你不能將大型應用拆分紅多個獨立的迷你型應用。我本身就結合 Redux爲企業級應用這樣作過。例如,我常常將分析面板、消息管理、系統管理、團隊/成員角色管理以及帳單管理等模塊拆分紅多個獨立的應用,每一個應用都有其本身的 Redux store。經過 API tokens 和 OAuth,這些應用共享同一個域下的登陸/session 管理,感受就像是一個統一的應用。

對於大多數應用,我建議默認使用 Redux。須要指出的是,Dan Abramov(Redux 的做者)在這一點上和我持相反的觀點。他喜歡應用盡量地保持簡單,這固然沒錯。傳統社區有句格言如是說:「除非真得感到痛苦,不然就別用 Redux」。

而個人觀點是:

「不知道本身正走在黑暗中的人是永遠不會去搜尋光明的「。

正如我說過的,在某些狀況下,Redux 比 setState() 更簡單。經過消除一切和共享的可變 state 以及同步依賴有關的 bug,Redux 簡化了 state 管理問題。

setState() 確定要學,但即便你不想使用 Redux,你也應該學學 Redux。不管你採用何種解決方案,它都能讓你重新的角度思考去應用的 state 管理問題,也可能能幫你簡化應用 state。

對於有大量衍生(derived ) state 的應用而言, MobX 可能會比 setState() 和 Redux 都要好,由於它很是擅於高效地管理和組織須要經過計算獲得的(calculated ) state 。

得利於其細粒度的、可觀察的訂閱模型,MobX也很擅於高效渲染大量(數以萬計)動態 DOM 節點。所以,若是你正在開發的是一款圖形遊戲,或者是一個監控全部企業級微服務實例的控制檯,那 MobX 多是個很好的選擇,它很是有利於實時地可視化展現這種複雜的信息。

接下來

想要全面學習如何用 React 和 Redux 開發軟件?

跟着 Eric Elliott 學 Javacript,機不可失時再也不來!

Eric Elliott「編寫 JavaScript 應用」 (O’Reilly) 以及 「跟着 Eric Elliott 學 Javascript」 兩書的做者。他爲許多公司和組織做過貢獻,例如 Adobe SystemsZumba FitnessThe Wall Street JournalESPNBBC等 , 也是不少機構的頂級藝術家,包括但不限於 Usher , Frank Ocean , Metallica

大多數時間,他都在 San Francisco Bay Area,同這世上最美的女子在一塊兒(譯註:這是怕老婆呢仍是怕老婆呢仍是怕老婆呢?)。

相關文章
相關標籤/搜索