[譯] setState() 門事件

檻外畸人發表於2017-04-06

React setState() 解惑

譯註:本文起因於作者的一條推特,他認為應該避免使用 setState(),隨後引發論戰,遂寫此文詳細闡明其觀點。譯者個人認為,本文主要在於“撕逼“,並未深入介紹 setState() 的技術細節,希望從技術層面深入瞭解 setState() 的同學可以參考[譯] React 未來之函式式 setState。對 setState() 不瞭解的同學可能會感到本文不知所云,特此說明。

[譯] setState() 門事件

一切都源於上週。3 位 React 初學者嘗試在專案中使用 setState() 時遇到了 3 種不同的問題。我指導過很多 React 新手,也為團隊提供從其他技術到 React 的架構轉型諮詢。

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

因此也就有了下面這條推特:

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

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

“我是 React 團隊的一員。在嘗試其他方法之前,請學會使用 setState。”

“那些所謂‘高手’們怕是要落伍了,因為 React 17 將會預設採用非同步排程。”

對於第二點:

“Fiber 有一種用於暫停、切分、重建和取消更新的策略,但如果你脫離了元件 state,那此策略便無法正常工作了。”

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

[譯] setState() 門事件

面對困境“呵呵”兩下並無妨,不過千萬別呵呵過後就對問題視而不見了。

在和另一個初學者交流的時候,我發現他也對 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() 門事件

“你就接著裝逼,你以為你所以為的就是你所以為的嗎?”

何時使用 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”。

而我的觀點是:

[譯] setState() 門事件

“不知道自己正走在黑暗中的人是永遠不會去搜尋光明的“。

正如我說過的,在某些情況下,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,機不可失時不再來!

[譯] setState() 門事件

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

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

相關文章