【譯】函式式的 setState 是 React 的未來

本文翻譯自: ofollow,noindex">Functional setState is the future of React – freeCodeCamp.org
譯者注:昨天自己有遇到一個 setState 的坑,就是前一篇文章裡記錄的,上網 Google 了下看到這一篇,關於 setState,這篇文章講解的很詳細深入:+1:,所以翻譯到掘金來,讓更多人可以看到。
更新:我在React Rally上就此主題進行了後續討論。雖然這篇文章更多的是關於“函式式的 setState”模式,但更多的是關於深入理解setState。
我在React Rally 上關於 setState 做的一個分享 Justice Mba - Demystifying setState() - YouTube
React在JavaScript中推廣了函數語言程式設計,這導致了大量的框架採用了React使用的基於元件的UI模式。如今,函式式熱潮正在蔓延到整個網路開發生態系統中。

譯者注:上述內容翻譯如下:
JavaScript生態系統正在從“本週的新框架”轉變為“新的(更快的)本週的React克隆”
ReactJS新聞@ReactJSNews
阿里巴巴釋出了他們自己的React-like。 顯著更小,更快 - 肯定有一個缺點! github.com/alibaba/rax
但React團隊遠沒有放鬆。他們繼續深入挖掘,探索更多的函式式寶石。
所以今天我向你透露一個隱藏在React中的新功能 - Functional setState!
好吧,這個名字只是我剛剛編造的......而且這並不是全新的或祕密。不,不完全是。其實它是React內建的一種模式,只有很少有開發人員知道這種模式。 它從來沒有名字,但現在它確實 - Functional setState!
通過Dan Abramov描述這種模式的話,Functional setState就是一種這樣的模式:
“與元件類分開宣告狀態更改。”
咦?
好吧......這些是你已經知道的了
React是一個基於元件的UI庫。元件基本上是一個接受一些屬性並返回UI元素的函式。
function User(props) { return ( <div>A pretty user</div> ); } 複製程式碼
元件可能需要擁有並管理其狀態。在這種情況下,您通常將元件編寫為類。然後你的狀態存在於類的 constructor
函式中:
class User { constructor () { this.state = { score : 0 }; } render () { return ( <div>This user scored {this.state.score}</div> ); } } 複製程式碼
為了管理狀態,React提供了一個名為 setState()
的特殊方法。你這樣使用它:
class User { ... increaseScore () { this.setState({score : this.state.score + 1}); } ... } 複製程式碼
請注意 setState()
的工作原理。您傳遞一個包含要更新的狀態部分的物件。換句話說,您傳遞的物件將具有與元件狀態中的鍵對應的鍵,然後 setState()
通過將物件合併到狀態來更新或設定狀態。這就是“set-State”
你可能不知道的
還記得我們說的 setState()
的工作原理嗎?那麼,如果我告訴你可以傳遞一個函式來代替傳遞一個物件呢?
是的。 setState()
也接受一個函式。該函式接受元件的先前 state 和 當前的 props,它用於計算並返回下一個 state。如下所示:
this.setState(function (state, props) { return { score: state.score - 1 } }); 複製程式碼
請注意, setState()
是一個函式,我們將另一個函式傳遞給它(函數語言程式設計...函式式 setState)。乍一看,程式碼可能看起來很醜陋,只有設定狀態的步驟太多了。但你為什麼還得這樣做?
為什麼要將函式傳遞給setState?
關鍵在於,狀態更新可能是非同步的。
想想呼叫 setState()
時會發生什麼。React將首先將傳遞給 setState()
的物件合併到當前狀態。然後它將開始合併。它將建立一個新的React Element樹(UI的物件表示),將新樹與舊樹進行區分,根據傳遞給 setState()
的物件找出已更改的內容,然後最終更新DOM。 呼!
這麼多工作!實際上,這甚至是一個過於簡化的摘要。但是相信React!
React does not simply “set-state”.
由於涉及的工作量很大,呼叫 setState()
可能不會立即更新您的狀態。
React可以將多個 setState()
的呼叫批處理成單個更新來提高效能。
上面這句話是什麼意思?
首先,“多次呼叫 setState()
”可能意味著在一個函式內多次呼叫 setState()
,如下所示:
state = {score : 0}; // multiple setState() calls increaseScoreBy3 () { this.setState({score : this.state.score + 1}); this.setState({score : this.state.score + 1}); this.setState({score : this.state.score + 1}); } 複製程式碼
現在,當React遇到“多次呼叫 setState()
”,而不是整整三次執行“set-state”時,React將避免我上面描述的大量工作並巧妙地對自己說:“不! 我不打算三次爬山,在每次旅行中攜帶和更新一些狀態。不,我寧願得到一個容器,將所有這些切片包裝在一起,只需更新一次。“這就是批處理!
請記住,傳遞給 setState()
的是一個普通物件。現在,假設任何時候React遇到“多次呼叫 setState()
”,它通過提取傳遞給每個 setState()
呼叫的所有物件來完成批處理,將它們合併在一起形成一個物件,然後使用該單個物件來執行 setState()
。
在JavaScript中,合併物件可能如下所示:
const singleObject = Object.assign( {}, objectFromSetState1, objectFromSetState2, objectFromSetState3 ); 複製程式碼
這種模式稱為物件組合。
在JavaScript中,“合併”或組合物件的方式是:如果三個物件具有相同的鍵,則傳遞給Object.assign()的最後一個物件的鍵值將作為該鍵最終的值。例如:
const me= {name : "Justice"}, you = {name : "Your name"}, we= Object.assign({}, me, you); we.name === "Your name"; //true console.log(we); // {name : "Your name"} 複製程式碼
因為 you
是合併到 we
的最後一個物件,所以 you
物件中的 name
值 - “Your name” - 將覆蓋 me
物件中 name
的值。
因此,如果使用物件作為引數多次呼叫 setState()
——每次傳遞一個物件——React將合併。換句話說,它將用我們傳遞的多個物件中組成一個新物件。 如果任何物件包含相同的鍵,則儲存具有相同鍵的最後一個物件的鍵的值。對嗎?
這意味著,鑑於我們上面的 increaseScoreBy3
函式,函式的最終結果將只是 1
而不是 3
,因為 React 沒有立即按我們呼叫 setState()
的順序更新狀態。首先,React將所有物件組合在一起,結果如下: {score:this.state.score + 1}
,然後只使用新組合的物件進行“set-state”一次。 像這樣: User.setState({score:this.state.score + 1}
。
To be super clear, passing object to setState() is not the problem here. The real problem is passing object to setState() when you want to calculate the next state from the previous state. So stop doing this. It’s not safe!
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state. Here is a pen by Sophia Shoemaker that demos this problem. Play with it, and pay attention to both the bad and the good solutions in this pen:
要非常清楚,將物件傳遞給 setState()
不是問題所在。真正的問題是當你想要從前一個狀態計算下一個狀態時,將物件傳遞給 setState()
。所以停止這樣做。這不安全!
因為 this.props
和 this.state
可以非同步更新,所以不應該依賴它們的值來計算下一個狀態。
索菲亞·舒梅克(Sophia Shoemaker)的這個例子可以演示這個問題。 演示它,並注意這個例子中的壞和好的解決方案。
函式式setState解決了我們的問題
如果你沒有花時間演示上面的例子,我強烈建議你還是先看一下,因為它將幫助你掌握這篇文章的核心概念。
當你演示了上面的例子,你無疑看到函式式setState解決了我們的問題。但究竟是怎麼做的呢?
我們來諮詢React的核心成員 - Dan。

請注意他給出的答案。
當你使用函式式setState ...
更新將被放進一個佇列,然後按呼叫順序執行。
因此,當React遇到“多次函式式 setState()
呼叫”時,React按照“呼叫它們的順序”對函式進行排隊,而不是將物件合併在一起,(當然沒有要合併的物件)。
之後,React繼續通過呼叫“佇列”中的每個函式來更新狀態,將它們傳遞給先前的狀態 - 即,在第一個函式setState()呼叫之前的狀態(如果當前是第一個函式setState()正在執行)或佇列中前一個函式setState()呼叫的最新更新的狀態。
下面我們將來模擬一個setState()方法,這是為了讓你瞭解React正在做什麼。另外,為了減少冗長,我們將使用ES6。如果需要,您隨時可以編寫ES5版本。
首先,讓我們建立一個元件類。然後,在其中,我們將建立一個假的setState()方法。此外,我們的元件將具有increaseScoreBy3()方法,該方法將執行多功能setState。最後,我們將例項化該類,就像React所做的那樣。
class User{ state = {score : 0}; //let's fake setState setState(state, callback) { this.state = Object.assign({}, this.state, state); if (callback) callback(); } // multiple functional setState call increaseScoreBy3 () { this.setState( (state) => ({score : state.score + 1}) ), this.setState( (state) => ({score : state.score + 1}) ), this.setState( (state) => ({score : state.score + 1}) ) } } const Justice = new User(); 複製程式碼
請注意,setState還接受可選的第二個引數 - 回撥函式。如果它存在,React在更新狀態後呼叫它。
現在,當用戶觸發 increaseScoreBy3()
時,React會將多個函式式setState放入佇列。我們不會在這裡偽造這種邏輯,因為我們的重點是 什麼才真的使功能setState安全 。但是你可以把“排隊”過程的結果想象成一個函式陣列,如下所示:
const updateQueue = [ (state) => ({score : state.score + 1}), (state) => ({score : state.score + 1}), (state) => ({score : state.score + 1}) ]; 複製程式碼
最後,讓我們來模擬更新過程:
// recursively update state in the order function updateState(component, updateQueue) { if (updateQueue.length === 1) { return component.setState(updateQueue[0](component.state)); } return component.setState( updateQueue[0](component.state), () => updateState( component, updateQueue.slice(1)) ); } updateState(Justice, updateQueue); 複製程式碼
沒錯,這不是一個很棒的程式碼。我相信你可以做得更好。但這裡的關鍵焦點是每次React執行函式setState中的函式時,React都會通過向其傳遞更新狀態的新副本來更新您的狀態。這使得函式setState可以基於先前的狀態設定狀態。 在這裡,我用完整的程式碼建立了一個bin。
修補它(可能使它看起來更性感),只是為了更好地理解它。
class User{ state = {score : 0}; //fake setState setState(state, callback) { console.log("state", state); this.state = Object.assign({}, this.state, state); if (callback) callback(); } } const Justice = new User(); const updateQueue = [ (state) => ({score : state.score + 1}), (state) => ({score : state.score + 1}), (state) => ({score : state.score + 1}) ]; // recursively update the state in the order function updateState(component, updateQueue) { if (updateQueue.length === 1) { return component.setState(updateQueue[0](component.state)); } return component.setState( updateQueue[0](component.state), () => updateState( component, updateQueue.slice(1)) ); } 複製程式碼
執行一下這段程式碼,確保你看懂它。當你回來時我們會看到是什麼讓函式式的setState真正變得閃閃發光。
這個祕訣我只告訴你哦
到目前為止,我們已經深入探討了為什麼在React中執行多個函式式setStates是安全的。但是我們實際上還沒有完成函式式setState的完整定義:“宣告狀態更改與元件類分開”。
多年來,setting-state的邏輯——即我們傳遞給setState()的函式或物件 - 總是存在於元件類中。這比宣告更為必要。
那麼今天,我向你展示新出土的寶藏 - 最好的React祕密:

感謝Dan Abramov!
這是函式式setState的強大功能。在元件類之外宣告狀態更新邏輯。然後在元件類中呼叫它。
// outside your component class function increaseScore (state, props) { return {score : state.score + 1} } class User{ ... // inside your component class handleIncreaseScore () { this.setState( increaseScore) } ... } 複製程式碼
這是宣告性的!您的元件類不再關心狀態更新。它只是宣告它想要的更新型別。
要深刻理解這一點,請考慮那些通常具有許多狀態切片的複雜元件,在不同操作更新每個切片。有時,每個更新功能都需要多行程式碼。所有這些邏輯都將存在於您的元件中。但以後不再是這樣了!
另外,如果你像我一樣,我喜歡讓每個模組都儘可能短,但現在你覺得你的模組太長了。現在,您可以將所有狀態更改邏輯提取到其他模組,然後匯入並在元件中使用它。
import {increaseScore} from "../stateChanges"; class User{ ... // inside your component class handleIncreaseScore () { this.setState( increaseScore) } ... } 複製程式碼
現在,您甚至可以在另一個元件中重用increaseScore函式。只需匯入它。
你還可以用函式式setState做什麼?
讓測試變得簡單!

你也可以傳遞額外的引數來計算下一個狀態(這個讓我大吃一驚...... )

期待更多......
多年來,React團隊一直在探索如何最好地實現有狀態的函式。 函式式setState似乎正是正確的答案(可能)。