[譯]React未來之函式式setState

玄學醬發表於2017-10-17
本文講的是[譯] React 未來之函式式 setState,

1*K8A3aXts5rTCHYRcdHIR6g.jpeg

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

1

但是 React 團隊卻還不“消停”,他們持續深耕,從 React(已經超神了!)中發掘出更多函數語言程式設計的寶藏。

因此本文將展示深藏在 React 中的又一函式式“寶藏” —— 函式式(functional)setState

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

正如 Dan Abramov 所言,在函式式 setState 模式中,“元件 state 變化的宣告可以和元件類本身獨立開來”。

這?

你已經知道的是…

React 是一個基於元件的 UI 庫,元件基本上可以看作是一個接受某些屬性然後返回 UI 元素的函式。

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

元件可能需要持有並管理其 state。在這種情況下,一般將元件編寫為一個類,然後在該類的constructor 函式中初始化 state:

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

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

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

class User {
  ...

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

  ...
}

注意 setState() 的作用機制:你傳遞給它一個物件,該物件含有 state 中你想要更新的部分。換句話說,該物件的鍵(keys)和元件 state 中的鍵相對應,然後 setState() 通過將該物件合併到 state 中來更新(或者說 sets)state。因此稱為 “set-State”。

你可能還不知道的是…

記住 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 中的,因此 you 的 name 屬性的值 “Your name” 會覆蓋me 的 name 屬性的值。因此 we 的 name 屬性的值最終為 “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.props 和 this.state 可能是非同步更新的,你不能依賴這些值計算下一個 state。

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

程式碼連結

讓函式式 setState 來拯救你

如果你還未曾把玩上面的例子,我還是強烈建議你玩一玩,因為這有利於你理解本文的核心概念。

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

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

1

注意看他給出的答案,當你編寫函式式 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。

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

1

一番把玩過後,讓我們來弄清為何將函式式 setState 稱之為“寶藏”。

React 最為深藏不露的祕密

至此,我們已經深入探討了為什麼多次函式式 setState 在 React 中是安全的。但是我們還沒有給函式式 setState 下一個完整的定義:“獨立於元件類之外宣告 state 的變化”。

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

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

1

感謝 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 還能用於何處呢?

簡化測試!

1

你還可以傳遞額外的引數用於計算下一個 state(這讓我腦洞大開…#funfunFunction)。

1

更多精彩,敬請期待…

React 未來式

0*uInBa_PPwz5aLo0j.jpg

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

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

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

1

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

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

Happy Coding!






原文釋出時間為:2017年3月20日

本文來自雲棲社群合作伙伴掘金,瞭解相關資訊可以關注掘金網站。


相關文章