[譯] Immer 下的不可突變資料和 React 的 setState

鄭昊川發表於2018-10-12

Immer 下的不可突變資料和 React 的 setState

Immer 是為 JavaScript 不可突變性打造的一個非常棒的全新庫。之前像 Immutable.js 這樣的庫,它需要引入操作你資料的所有新方法。

它很不錯,但是需要複雜的介面卡並在 JSON 和 不可突變 之間來回轉換,以便在需要時與其他庫一起使用。

Immer 簡化了這一點,你可以像往常一樣使用資料和 JavaScript 物件。這意味著當你需要考慮效能並且想知道資料何時發生了變更,你可以使用三個等號來做嚴格的全等檢查以及證明資料的確發生了變更。

你對 shouldComponentUpdate 的呼叫不再需要使用雙等或者全等去遍歷整個資料並進行比較。

文章截圖

[譯] Immer 下的不可突變資料和 React 的 setState

注:此處為截圖,原文為視訊,建議看英文原文。

物件展開運算子

在最新版本的 JavaScript 中,許多開發者依賴物件展開運算子來實現不可突變性。例如,你可以展開之前的物件並覆蓋特定的屬性,或者增加新的屬性。它會在底層使用 Object.assign 並返回一個新物件。


const prevObject = {
  id: "12345",
  name: "Jason",
};

const newObject = {
  ...prevObject,
  name: "Jason Brown",
};
複製程式碼

我們的 newObject 現在會是一個完全不同的物件,所以任何全等判斷(prevObject === newObject)將會返回 false。所以它完全建立了一個新物件。name 屬性也不再是 Jason 而是會變成 Jason Brown,而且由於我們沒有對 id 屬性進行任何操作,所以它會保持不變。

這也適用於 React,因為 React 只會合併最外層的屬性,所以當你在 state 中有巢狀的物件時,你需要對之前的物件進行展開操作和更新。

讓我們看一個例子。可以看到我們有兩個巢狀的計數器,但是我們只想更新其中的一個而不影響另一個。

import React, { Component } from "react";

class App extends Component {
  state = {
    count: {
      counter: 0,
      otherCounter: 5,
    },
  };

  render() {
    return <div className="App">{this.state.count.counter}</div>;
  }
}

export default App;
複製程式碼

下一步在 componentDidMount 鉤子中,我們將設定一個間隔定時器來更新我們巢狀的計數器。不過,我們希望保持 otherCounter 的值不變。所以,我們需要使用物件展開運算子來把它從以前巢狀的 state 中帶過來。

componentDidMount() {
    setInterval(() => {
      this.setState(state => {
        return {
          count: {
            ...state.count,
            counter: state.count.counter + 1,
          },
        };
      });
    }, 1000);
  }
複製程式碼

這在 React 中是一個非常常見的場景。而且,如果你的資料是巢狀的非常深的,當你需要展開多個層級時,它會增加複雜性。

Immer Produce 基礎

Immer 仍然允許使用突變(直接改變值)而完全無需擔心如何去管理展開的層級,或者哪些資料我們觸及過以及需要維持不可突變性。

讓我們設定一個場景:你向計數器傳遞一個值來進行遞增,與此同時,我們還有一個 user 物件是不需要被觸及的。

這裡我們渲染我們的應用並傳遞增量值。

ReactDOM.render(<App increaseCount={5} />, document.getElementById("root"));
複製程式碼
import React, { Component } from "react";

class App extends Component {
  state = {
    count: {
      counter: 0,
    },
    user: {
      name: "Jason Brown",
    },
  };

  componentDidMount() {
    setInterval(() => {}, 1000);
  }

  render() {
    return <div className="App">{this.state.count.counter}</div>;
  }
}

export default App;
複製程式碼

我們像之前那樣設定了我們的應用,現在我們有一個 user 物件和一個巢狀的計數器。

我們將匯入 immer 並把它的預設值賦給 produce 變數。在給定當前 state 時,它將幫助我們建立下一個 state。

import produce from "immer";
複製程式碼

接下來,我們將建立一個叫做 counter 的函式,它接收 state 和 props 作為引數,這樣我們就可以讀取當前的計數,並基於 increaseCount 屬性更新我們的下一次計數。

const counter = (state, props) => {};
複製程式碼

Immer 的 produce 方法接收 state 作為第一個引數,以及一個為下一個狀態改變資料的函式作為第二個引數。

produce(state, draft => {
  draft.count.counter += props.increaseCount;
});
複製程式碼

如果你現在把他們放在一起。我們就可以建立計數器函式,它接收 state 和 props 並呼叫 produce 函式。然後我們按照對下一次狀態期望的樣子去改變 draft。Immer 的 produce 函式將為我們建立一個新的不可突變狀態。

const counter = (state, props) => {
  return produce(state, draft => {
    draft.count.counter += props.increaseCount;
  });
};
複製程式碼

我們更新後的間隔計數器函式大概會是這樣。

componentDidMount() {
    setInterval(() => {
      const nextState = counter(this.state, this.props);
      this.setState(nextState);
    }, 1000);
  }
複製程式碼

不過我們只是觸及過 countcounter,我們的 user 物件上又發生了什麼呢?物件的引用是否也發生了變化?答案是否定的。Immer 確切的知道哪些資料是被觸及過的。所以,如果我們在元件更新之後進行一次全等檢測,我們可以看到 state 中之前的 user 物件和之後的 user 物件是完全相同的。

componentDidUpdate(prevProps, prevState) {
    console.log(this.state.user === prevState.user); // Logs true
  }
複製程式碼

當你考慮效能而使用 shouldComponentUpdate 時,或者類似於 React Native 中FlatList 那樣,需要一種簡單的方式來知道某一行是否已經更新時,這就非常的重要。

Immer 柯里化

Immer 可以使得操作更加簡單。如果它發現你傳遞的第一個引數是一個函式而不是一個物件,它就會為你建立一個柯里化的函式。因此,produce 函式返回另一個函式而不是一個新物件。

當它被呼叫時,它會把第一個引數用作你希望改變的 state,然後還會傳遞任何其他引數。

因此,它不僅僅是可以建立一個計數器函式的(工廠)函式,就連 props 也會被代理。

const counter = produce((draft, props) => {
  draft.count.counter += props.increaseCount;
});
複製程式碼

得益於 produce 返回一個函式,我們可以直接把它傳遞給 setState,因為 setState 有接收函式作為引數的能力。當你正在引用之前的狀態時,你應該使用函式化的 setState(函式作為第一個引數)。在我們的場景中,我們需要引用之前的計數來把它增加到新的計數。它將傳遞當前的 state 和 props 作為引數,這也正是設定我們的 counter 函式所需要的。

所以我們的間隔計數器僅需要 this.setState 接收 counter 函式即可。

componentDidMount() {
    setInterval(() => {
      this.setState(counter);
    }, 1000);
  }
複製程式碼

總結

[譯] Immer 下的不可突變資料和 React 的 setState

這顯然是一個人為的示例,但具有廣泛的現實應用。可以輕鬆比較僅更新了單個欄位的一長串列表資料。大型巢狀表單只需要更新觸及過的特定部分。

你不再需要做淺比對或者深比對,而且你現在可以做全等檢查來準確的知道你的資料是否發生了變化,而後決定是否需要重新渲染。


Originally published at Code.

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章