React 中的寶藏:steState 函數
React 已經在 JavaScript 中普及了函數式編程。 這導致一些大型框架采用了 React 使用的基于組件的 UI 模式。 現在功能性發燒已經蔓延到整個網絡開發生態系統中。
但 React 團隊并沒有停下腳步,他們繼續深入挖掘,發現更多隱藏在神奇庫中的強大函數。
所以今天我向你透露一個新函數,它是 React 中的黃金寶藏 —— setState!
它并不是全新的,只是內置于 React 的模式,難以被開發者發掘。
Dan Abramov 將 Functional setState 模式描述為:
與組件類分開聲明狀態更改。
基本概念
React 是一個基于組件的 UI 庫。組件是一個接受一些屬性并返回一個 UI 元素的函數。
function User(props){
return(
<div>漂亮的使用者</ div>
);
}}
組件需要具有和管理其狀態的能力。在這種情況下,通常將組件寫為類。constructor 函數中就有了它的狀態:
class User {
constructor(){
this.state = {
分數:0
};
} render(){
return(
<div>此用戶評分為{this.state.score} </ div>
);
}}
}}
為了管理狀態,React 提供了一個名為 setState()的特殊方法:
class User {
... increaseScore(){
this.setState({score:this.state.score + 1});
} ...
}}
注意 setState()的工作方式。你傳遞一個對象,其中包含你要更新的狀態的一部分。換句話說,傳遞的對象將具有與組件狀態中的鍵相對應的鍵,然后 setState()通過將對象合并到狀態來更新或設置狀態。因此叫“ 狀態設置(set-State) ”。
你可能不知道的部分
還記得我們介紹的 setState() 的工作原理嘛? 除了其中的傳遞對象,我們還可以傳遞函數。
setState() 也接受一個函數作為參數。 該函數獲取組件的 previous 狀態和 current 屬性,用于計算和返回下一個狀態。 請看下面代碼:
this.setState(function (state, props) {
return {
score: state.score - 1
}
});
注意 setState() 是一個函數,我們將另一個函數傳遞給它作為參數(函數式編程...函數式 setState)。
初看這一操作似乎并不怎么樣,調用 set-state 需要太多步驟了。那為什么要這樣做呢?
為什么要給 setState 傳遞函數?
想想當 setState () 被調用時發生了些什么。 React 首先將你傳遞給 setState() 函數的對象合并到當前狀態中。然后它將開始協調各部分。它將創建一個 React 元素樹(用于表示 UI 的對象),對比新舊樹的差別,并根據你傳遞給 setState() 函數對象的不同,計算出相應的變化部分,最終完成 DOM 更新。
工作量真大!事實上,這僅僅是一個簡化版的總結。
React 并不只是簡單的“set-state”。
因為包含大量的工作,調用 setState() 可能并不會立即更新你的 state。
React 可能會出于性能考慮將多個 setState() 調用合并成一個批處理更新操作。
這樣做對 React 而言意味著什么呢?
首先,“多個 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"——它才不會像我上面說的那樣去做如此龐大工作量的事情呢!它會對自己說:“不!我不想每次爬山時只帶著一部分狀態更新,然后去爬三次山。我更想要有一個容器,用來把這些狀態一起打包全放在里面,然后一次性地把它們都帶到山上去!”而這就是“批處理”(batching)。
我們傳給 setState() 的是一個樸素的對象(plain object)。現在,假設任何時候當 React 遇到“多次 setState() 調用”時,它都會執行批處理,即提取所有單次傳遞給 setState() 的對象,把它們合并在一起形成一個新的單一的對象,并用這個單一的對象去做 setState() 的事情。
在 JavaScript 里面,合并對象可能會如同下面這種形式:
const singleObject = Object.assign(
{},
objectFromSetState1,
objectFromSetState2,
objectFromSetState3
);
上面這種模式就是我們所熟知的對象組合(object composition)。
JavaScript 中,合并(merging)或組合(composing)對象是這樣工作的:如果三個對象有相同的 key, 傳給 Object.assign() 的最后一個包含此 key 的對象會覆蓋掉前面的值。例如:
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 值。 所以呢,you 贏啦!:)
你看,當你多次往 setState() 里傳入對象調用此方法時,是每次傳一個對象,React 會把這些對象合并。換句話說,它會基于我們傳入的這多個對象來組合出一個新的對象。并且如果這多個對象有相同的 key, 最后一次傳入的對象的 key 值會被儲存下來。對嗎?
這意味著,以我們上面的 increaseScoreBy3 函數為例,函數的最終結果將只是 1 而不是 3,因為 React 沒有立即按照我們調用 setState() 的順序來更新 state。但是首先,React 將所有對象組合在一起,結果是:{score:this.state.score + 1},那么使用新組合的對象僅完成一次“set-state”。 像這樣:User.setState({score:this.state.score + 1}。
更明確來說,將對象傳遞給 setState() 并不是問題所在。真正的問題在于當你想從前一個狀態計算下一個狀態時,傳遞給 setState() 的對象。這么做的風險大,理應停止。
因為 this.props 和 this.state 可能會異步更新,所以你不應該依賴它們的值來計算下一個狀態。
函數式的setState能拯救世界
Sophia Shoemaker 在 Pen 中演示了這個問題:
點擊查看這一 Pen 能幫助你更好地了解這個問題。當你查看后你會發現 setState 解決了我們的問題。但是確切地說,是怎么解決的呢?
“狀態更新”會被排列,然后按照它們被調用的順序來執行。
所以,當 React 碰到“多次 setState() 調用”的情況時,它不會把對象合并在一起(當然了,這里并沒有什么對象要被合并),它會按照調用的順序把這些方法排個隊。
接下來,React 依次調用隊列中的方法,把上一個狀態傳遞給當前的方法,從而不斷更新狀態。這里提到的“上一個狀態”,分兩種情況:
-
對隊列中第一個被執行的 setState() 而言,那就是在其被執行前的對象的本來狀態
-
對隊列中非第一個 setState() 而言,那就是隊列里離它最近的 setState() 執行后生成的對象的狀態。
我想直接拿代碼來闡釋。但這次只是模擬,目的是傳達 React 的操作。
為避免冗雜,我將使用 ES6 語法。
首先,我們創建一個組件類。然后,在這個類里,我們創建一個 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 執行函數,并通過傳遞已更新狀態的新副本來更新您的狀態。 這使得功能 setState 可以基于先前狀態設置狀態。
這里我用完整的代碼做了一個 bin。 Tinker 只為了使代碼更完善。
來自:https://www.oschina.net/translate/functional-setstate-is-the-future-of-react