[譯] React 將來之函數式 setState

React 使得函數式編程在 JavaScript 領域流行了起來,這驅使大量框架採用 React 所推崇的基於組件的編程模式,函數式編程熱正在大範圍涌向 web 開發領域。javascript

可是 React 團隊卻還不「消停」,他們持續深耕,從 React(已經超神了!)中發掘出更多函數式編程的寶藏。html

所以本文將展現深藏在 React 中的又一函數式「寶藏」 —— 函數式(functional)setState前端

好吧,名字實際上是我亂編的,並且這個技術也稱不上是新事物或者是個祕密。這一模式內建於 React 中,可是隻有少數 React 深耕者才知道,並且從未有過正式名稱 —— 不過如今它有了,那就是函數式 setStatejava

正如 Dan Abramov 所言,在函數式 setState 模式中,「組件 state 變化的聲明能夠和組件類自己獨立開來」。react

這?android

你已經知道的是...

React 是一個基於組件的 UI 庫,組件基本上能夠看做是一個接受某些屬性而後返回 UI 元素的函數。ios

function User(props) {
  return (
    <div>A pretty user</div>
  );
}複製代碼

組件可能須要持有並管理其 state。在這種狀況下,通常將組件編寫爲一個類,而後在該類的 constructor 函數中初始化 state:git

class User {
  constructor () {
  this.state = {
      score : 0
    };
  }

  render () {
    return (
      <div>This user scored **{this.state.score}**</div>
    );
  }
}複製代碼

React 提供了一個用於管理 state 的特殊函數 —— setState(),其用法以下:github

class User {
  ...

  increaseScore () {
  this.setState({score : this.state.score + 1});
  }

  ...
}複製代碼

注意 setState() 的做用機制:你傳遞給它一個對象,該對象含有 state 中你想要更新的部分。換句話說,該對象的鍵(keys)和組件 state 中的鍵相對應,而後 setState() 經過將該對象合併到 state 中來更新(或者說 sets)state。所以稱爲 「set-State」。web

你可能還不知道的是...

記住 setState() 的做用機制了嗎?若是我告訴你說,setState() 不只能接受一個對象,還能接受一個函數做爲參數呢?

沒錯,setState() 確實能夠接受一個函數做爲參數。該函數接受該組件前一刻的 state 以及當前的 props 做爲參數,計算和返回下一刻的 state。以下所示:

this.setState(function (state, props) {
 return {
  score: state.score - 1
 }
});複製代碼

注意 setState() 自己是一個函數,並且咱們傳遞了另外一個函數給它做爲參數(函數式編程,函數式 setState)。乍一看可能以爲這樣寫挺醜陋的,set-state 須要的步驟太多了。那爲何還要這樣寫呢?

爲何傳遞一個函數給 setState?

理由是,state 的更新多是異步的

思考一下調用 setState()發生了什麼。React 首先會將你傳遞給 setState() 的參數對象合併到當前 state 對象中,而後會啓動所謂的 reconciliation,即建立一個新的 React Element tree(UI 層面的對象表示),和以前的 tree 做比較,基於你傳遞給 setState() 的對象找出發生的變化,最後更新 DOM。

呦!工做不少嘛!實際上,這還只是精簡版總結。但必定要相信:

React 不會僅僅簡單地 「set-state」。

考慮到所涉及的工做量,調用 setState() 並不必定會即時更新 state。

考慮到性能問題,React 可能會將屢次 setState() 調用批處理(batch)爲一次 state 的更新。

這又意味着什麼呢?

首先,「屢次 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});
    }

    ...複製代碼

面對這種 屢次 setState() 調用 的狀況,爲了不重複作上述大量的工做,React 並不會真地完整調用三次 "set-state";相反,它會機智地告訴本身:「哼!我纔不要‘愚公移山’三次呢,每次還得更新部分 state。不行,我得找個‘揹包’,把這些部分更新打包裝好,一次性搞定。」朋友們,這就是所謂的批處理啊!

記住傳遞給 setState() 的純粹是個對象。如今,假設 React 每次遇到 屢次 setState() 調用都會做上述批處理過程,即將每次調用 setState() 時傳遞給它的全部對象合併爲一個對象,而後用這個對象去作真正的 setState()

在 JavaScript 中,對象合併能夠這樣寫:

const singleObject = Object.assign(
  {},
  objectFromSetState1,
  objectFromSetState2,
  objectFromSetState3
);複製代碼

這種寫法叫做 object 組合(composition)

在 JavaScript 中,對象「合併(merging)」或者叫對象組合(composing)的工做機制以下:若是傳遞給 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 中的,所以 youname 屬性的值 「Your name」 會覆蓋 mename 屬性的值。所以 wename 屬性的值最終爲 「Your name」,因此說 you 勝了!

綜上所述,若是你屢次調用 setState() 函數,每次都傳遞給它一個對象,那麼 React 就會將這些對象合併。也就是說,基於你傳進來的多個對象,React 會組合出一個新對象。若是這些對象有同名的屬性,那麼就會取最後一個對象的屬性值,對吧?

這意味着,上述 increaseScoreBy3 函數的最終結果會是 1 而不是 3。由於 React 並不會按照 setState() 的調用順序即時更新 state,而是首先會將全部對象合併到一塊兒,獲得 {score : this.state.score + 1},而後僅用該對象進行一次 「set-state」,即 User.setState({score : this.state.score + 1}

須要搞清楚的是,給 setState() 傳遞對象自己是沒有問題的,問題出在當你想要基於以前的 state 計算出下一個 state 時還給 setState() 傳遞對象。所以可別這樣作了,這是不安全的!

由於 this.propsthis.state 多是異步更新的,你不能依賴這些值計算下一個 state。

下面 Sophia Shoemaker 寫的一個例子展現了上述問題,細細把玩一番吧,留意其中好壞兩種解決方案。

代碼連接

讓函數式 setState 來拯救你

若是你還不曾把玩上面的例子,我仍是強烈建議你玩一玩,由於這有利於你理解本文的核心概念。

在把玩上述例子的時候,你確定注意到了 setState 解決了咱們的問題。但到底是如何解決的呢?

讓咱們請教一下 React 界的 Oprah(譯者注:非知名脫口秀主持人)—— Dan。

注意看他給出的答案,當你編寫函數式 setState 的時候,

更新操做會造成一個任務隊列,稍後會按其調用順序依次執行。

所以,當面對屢次函數式 setState() 調用時,React 並不會將對象合併(顯然根本沒有對象讓它合併),而是會按調用順序將這些函數排列起來。

以後,React 會依次調用隊列中的函數,傳遞給它們前一刻的 state —— 若是當前執行的是隊列中的第一個函數式 setState() ,那麼就是在該函數式 setState() 調用以前的 state;不然就是最近一次函數式 setState() 調用並更新了 state 以後的 state。經過這種機制,React 達到 state 更新的目的。

話說回來,我仍是以爲代碼更有說服力。只不過此次咱們會「僞造」點東西,雖然這不是 React 內部真正的作法,但也基本是這麼個意思。

還有,考慮到代碼簡潔問題,下面會使用 ES6,固然你也能夠用 ES5 重寫一下。

首先,建立一個組件類。在這個類裏,建立一個僞造setState() 方法。該組件會使用 increaseScoreBy3() 方法來屢次調用函數式 setState。最後,會仿照 React 的作法實例化該類。

class User{
  state = {score : 0};

  //「僞造」 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 就會在 state 更新後調用它。

如今,當用戶調用 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);複製代碼

誠然,這些代碼並不能稱之爲優雅,你確定能寫得更好。但核心概念是,使用函數式 setState,你能夠傳遞一個函數做爲其參數,當執行該函數時,React 會將更新後的 state 複製一份並傳遞給它,這便起到了更新 state 的做用。基於上述機制,函數式 setState 即可基於前一刻的 state 來更新當前 state。

下面是這個例子的完整代碼,請細細把玩以充分理解上述概念(或許還能夠改得更優雅些)。

一番把玩事後,讓咱們來弄清爲什麼將函數式 setState 稱之爲「寶藏」。

React 最爲深藏不露的祕密

至此,咱們已經深刻探討了爲何屢次函數式 setState 在 React 中是安全的。可是咱們尚未給函數式 setState 下一個完整的定義:「獨立於組件類以外聲明 state 的變化」。

過去幾年,setting-state 的邏輯(即傳遞給 setState() 的對象或函數)一直都存在於組件類內部,這更像是命令式(imperative)而非 聲明式(declarative)。(譯者注:imperative 和 declarative 的區別參見 stackoverflow上的問答

不過,今天我將向你展現新出土的寶藏 —— React 最爲深藏不露的祕密

感謝 Dan Abramov

這就是函數式 setState 的強大之處 —— 在組件類外部聲明 state 的更新邏輯,而後在組件類內部調用之。

// 在組件類以外
function increaseScore (state, props) {
  return {score : state.score + 1}
}

class User{
  ...

// 在組件類以內
  handleIncreaseScore () {
    this.setState(increaseScore)
  }

  ...
}複製代碼

這就叫作 declarative!組件類不用再關心 state 該如何更新,它只須聲明它想要的更新類型便可。

爲了充分理解這樣作的優勢,不妨設想以下場景:你有一些很複雜的組件,每一個組件的 state 都由不少小的部分組成,基於 action 的不一樣,你必須更新 state 的不一樣部分,每個更新函數都有不少行代碼,而且這些邏輯都存在於組件內部。不過有了函數式 setState,不再用面對上述問題了!

此外,我我的偏心小而美的模塊;若是你和我同樣,你就會以爲如今這模塊略顯臃腫了。基於函數式 setState,你就能夠將 state 的更新邏輯抽離爲一個模塊,而後在組件中引入和使用該模塊。

import {increaseScore} from "../stateChanges";

class User{
  ...

  // 在組件類以內
  handleIncreaseScore () {
    this.setState(increaseScore)
}

  ...
}複製代碼

並且你還能夠在其餘組件中複用 increaseScore 函數 —— 只須引入模塊便可。

函數式 setState 還能用於何處呢?

簡化測試!

你還能夠傳遞額外的參數用於計算下一個 state(這讓我腦洞大開...#funfunFunction)。

更多精彩,敬請期待...

React 將來式

最近幾年,React 團隊一直都致力於更好地實現 stateful functions

函數式 setState 看起來就是這個問題的正確答案(也許吧)。

Hey, Dan!還有什麼最後要說的嗎?

若是你閱讀至此,估計就會和我同樣興奮了。即刻開始體驗函數式 setState 吧!

歡迎擴散,歡迎吐槽(Twitter)。

Happy Coding!

掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索