本文翻譯自:Functional setState is the future of React – freeCodeCamp.org
譯者注:昨天自己有遇到一個 setState 的坑,就是【React 踩坑記】setState 這樣用可能會出錯!這篇文章裡記錄的,上網 Google 了下看到這一篇,關於 setState,這篇文章講解的很詳細深入?,所以翻譯到掘金來,讓更多人可以看到。
更新:我在React Rally上就此主題進行了後續討論。雖然這篇文章更多的是關於“函式式的 setState”模式,但更多的是關於深入理解setState。
我在React Rally 上關於 setState 做的一個分享 Justice Mba - Demystifying setState() - YouTube
React在JavaScript中推廣了函數語言程式設計,這導致了大量的框架採用了React使用的基於元件的UI模式。如今,函式式熱潮正在蔓延到整個 web 開發生態系統中。
譯者注:上述內容翻譯如下:
JavaScript生態系統正在從“本週的新框架”轉變為“新的(更快的)本週的React克隆”
ReactJS新聞@ReactJSNews
阿里巴巴釋出了他們自己的類React框架,似乎是更輕量更快的 - 但是肯定有一個缺點!github.com/alibaba/rax
但React團隊遠沒有放鬆。他們繼續深入挖掘,探索更多的函式式寶石。
所以今天我向你透露一個隱藏在React中的函式式寶石 - 函式式 setState!
好吧,這個名字只是我剛剛編造的......而且這並不是全新的東西或祕密。不,不完全是。其實它是React內建的一種模式,只有很少有開發人員知道這種模式。 它從來沒有名字,但現在它可以叫做 - 函式式 setState!
正如Dan Abramov所描述的,函式式 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()
的工作原理。您傳遞一個包含要更新的 state 部分的物件。換句話說,您傳遞的物件將具有與元件 state 中的鍵對應的鍵,然後setState()
通過將物件合併到 state 來更新或設定 state。這就是“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};
// 多次呼叫`setState()
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}
。
要非常清楚,將物件傳遞給setState()
不是問題所在。真正的問題在於當你想要基於前一個狀態計算下一個狀態時,將物件傳遞給setState()
。所以停止這樣做。這不安全!
因為
this.props
和this.state
可以非同步更新,所以不應該依賴它們的值來計算下一個狀態。
索菲亞·舒梅克(Sophia Shoemaker)的這個例子可以演示這個問題。 演示它,並注意這個例子中的壞和好的解決方案。
函式式setState解決了我們的問題
如果你沒有花時間演示上面的例子,我強烈建議你還是先看一下,因為它將幫助你掌握這篇文章的核心概念。
當你演示了上面的例子,你無疑看到函式式setState解決了我們的問題。但究竟是怎麼做的呢?
我們來諮詢React的核心成員 - Dan。
Dan的twitter請注意他給出的答案。
當你使用函式式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();
}
// 多次函式式 setState 的呼叫
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})
];
複製程式碼
最後,讓我們來模擬更新過程:
// 按順序遞迴呼叫 state 的更新
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 都會通過向其傳遞更新 state 的新副本來更新您的狀態。這使得函式 setState 可以基於前一次的 state 來設定新的 state。 在這裡,我用完整的程式碼建立了一個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})
];
// 按順序遞迴呼叫 state 的更新
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的未來
多年來,React團隊一直在探索如何最好地實現有狀態的函式。 函式式setState似乎正是正確的答案(可能)。嘿,丹!最後(再說)一句話(來展望下 React)?
這條推的地址如果你已經看到這裡,你可能會像我一樣興奮。立即開始嘗試使用函式式setState!
快樂擼碼!