【譯】函數式的 setState 是 React 的將來

本文翻譯自:Functional setState is the future of React – freeCodeCamp.orgreact

譯者注:昨天本身有遇到一個 setState 的坑,就是【React 踩坑記】setState 這樣用可能會出錯!這篇文章裏記錄的,上網 Google 了下看到這一篇,關於 setState,這篇文章講解的很詳細深刻👍,因此翻譯到掘金來,讓更多人能夠看到。git

更新:我在React Rally上就此主題進行了後續討論。雖然這篇文章更多的是關於「函數式的 setState」模式,但更多的是關於深刻理解setState。github

我在React Rally 上關於 setState 作的一個分享 Justice Mba - Demystifying setState() - YouTubeweb

React在JavaScript中推廣了函數式編程,這致使了大量的框架採用了React使用的基於組件的UI模式。現在,函數式熱潮正在蔓延到整個 web 開發生態系統中。編程

譯者注:上述內容翻譯以下:數組

JavaScript生態系統正在從「本週的新框架」轉變爲「新的(更快的)本週的React克隆」安全

ReactJS新聞@ReactJSNews
阿里巴巴發佈了他們本身的類React框架,彷佛是更輕量更快的 - 可是確定有一個缺點!github.com/alibaba/rax框架

但React團隊遠沒有放鬆。他們繼續深刻挖掘,探索更多的函數式寶石。異步

因此今天我向你透露一個隱藏在React中的函數式寶石 - 函數式 setStateide

好吧,這個名字只是我剛剛編造的......並且這並非全新的東西或祕密。不,不徹底是。其實它是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.propsthis.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!

快樂擼碼!

相關文章
相關標籤/搜索