【譯】函式式的 setState 是 React 的未來

去衝浪鴨發表於2018-10-25

【譯】函式式的 setState 是 React 的未來

本文翻譯自: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 開發生態系統中。

【譯】函式式的 setState 是 React 的未來

譯者注:上述內容翻譯如下:

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.propsthis.state可以非同步更新,所以不應該依賴它們的值來計算下一個狀態。

索菲亞·舒梅克(Sophia Shoemaker)的這個例子可以演示這個問題。 演示它,並注意這個例子中的壞和好的解決方案。

函式式setState解決了我們的問題

如果你沒有花時間演示上面的例子,我強烈建議你還是先看一下,因為它將幫助你掌握這篇文章的核心概念。

當你演示了上面的例子,你無疑看到函式式setState解決了我們的問題。但究竟是怎麼做的呢?

我們來諮詢React的核心成員 - Dan。

【譯】函式式的 setState 是 React 的未來
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祕密:

【譯】函式式的 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做什麼?

讓測試變得簡單!

【譯】函式式的 setState 是 React 的未來
這條推的地址

你也可以傳遞額外的引數來計算下一個狀態(這個讓我大吃一驚...... )

【譯】函式式的 setState 是 React 的未來
這條推的地址

期待更多......

React的未來

【譯】函式式的 setState 是 React 的未來
多年來,React團隊一直在探索如何最好地實現有狀態的函式。 函式式setState似乎正是正確的答案(可能)。

嘿,丹!最後(再說)一句話(來展望下 React)?

【譯】函式式的 setState 是 React 的未來
這條推的地址

如果你已經看到這裡,你可能會像我一樣興奮。立即開始嘗試使用函式式setState!

快樂擼碼!

相關文章