2020要用immer來代替immutable優化你的React項目

不可變數據

React的老手們早就知道爲什麼要用不可變數據了,但是爲了防止新手們看不懂,所以還是要解釋一下什麼是不可變數據,不可變數據指的其實就是當你修改一個數據的時候,這個數據會給你返回一個新的引用,而自己的引用保持不變,有點像是經常用到的數組的map方法:

const arr1 = [1, 2, 3];
const arr2 = arr1.map(item => item * 10);

console.log(arr1 === arr2)
//false

這樣的話每次修改數據,新返回的數據就和原來不相等了。

如果數據變更,節點類型不相同的時候會怎樣呢?React 的做法非常簡單粗暴,直接將 原 VDOM 樹上該節點以及該節點下所有的後代節點 全部刪除,然後替換爲新 VDOM 樹上同一位置的節點,當然這個節點的後代節點也全都跟着過來了。

這樣的話非常浪費性能,父組件數據一變化,子組件全部都移除,再換新的,所以纔有了shouldComponentUpdate這個生命週期(Vue的小夥伴請放心,Vue原理和React不太一樣,所以沒這毛病),這個函數如果返回false的話子組件就不會更新,但是每次在這個函數裏面寫對比會很麻煩,所以有了PureComponent和Memo,但是隻提供了淺比較,所以這時候不可變數據就派上用場了,每次修改數據都和原數據不相等的話,就可以精確的控制更新。

immutable

Facebook早就知道React這一缺陷,所以歷時三年打造了一個不可變數據的immutable.js。它內部實現了一套完整的 Persistent Data Structure,還有很多易用的數據類型。像Collection、List、Map、Set、Record、Seq。有非常全面的map、filter、groupBy、reduce``find函數式操作方法。同時 API 也設計的和JS對象、數組等類似。
不過功能雖全,但是如果我們僅僅只是爲了優化淺對比防止子組件過度刷新的話,引入這麼大的一個庫就未免有些大材小用了,而且學習成本也是需要考慮在內的,所以要爲大家介紹一下今天的主角:輕量、易用、簡潔又可以快速上手的immer.js

immer

immer這玩意來頭可不小,他的創造者就是大名鼎鼎的Mobx作者,聽過Mobx的人應該都知道,它與Redux相比更簡潔、更輕量、同時也更加易學,所以immer也同樣的繼承了這些優點:輕量、簡潔、易上手、並且使用起來也非常的舒服,不會產生容易把immutable數據類型與原生JS數據類型搞混的情況。它的核心思想就是利用Vue3源碼中大量運用的Proxy代理,幾乎以最小的成本實現了JS的不可變數據結構,解決了許多日常開發中的棘手問題,相信看完我的文章你一定會喜歡上它的!
首先第一步就是先進行安裝:

npm i -S immer

或者

yarn add immer
import produce from 'immer';

const array = [{value: 0}, {value: 1}, {value: 2}];
const arr = produce(array, draft => {
  draft[0].value = 10;
});

console.log(arr === array);
//false

解釋一下:produce是生產的意思(你想起啥名都行,但是官網喜歡這麼叫,我就跟着這麼起名),這個函數第一個參數是你想要改變的數據對象,第二個參數是一個函數,這個函數的參數draft是草稿的意思,代表的就是你想要改變的那個數據對象,然後在函數體內你就正常想怎麼改就怎麼改,produce運行完的結果就是一個全新的對象啦!怎麼樣是不是超級簡潔超級好用呢?

  • 注意:如果你什麼也不返回或者並沒有操作數據的話,並不會返回一個新的對象!
const array = [{value: 0}, {value: 1}, {value: 2}];
const arr = produce(array, draft => {});

console.log(array === arr);
// true


引用一張immutable的圖,從圖中可以看出來返回值並不是一份深拷貝內容,而是共享了未被修改的數據,這樣的好處就是避免了深拷貝帶來的極大的性能開銷問題,並且更新後返回了一個全新的引用,即使是淺比對也能感知到數據的改變。

  • 如果把produce的第一個參數省略掉的話,只傳入第二個參數返回值將會是一個函數👇
const array = [{value: 0}, {value: 1}, {value: 2}];
const producer = produce((draft) => {
  draft[0].value = 10;
});
const arr = producer(array);

console.log(array === arr);
// false

這樣雖然結果一樣,但是卻增強了可複用性,甚至可以進行再次封裝來形成一個高階函數:

const array = [{value: 0}, {value: 1}, {value: 2}];
const producer = (state, fn) => produce(fn)(state);
const arr = producer(array, draft => { draft[0] = 666 });

console.log(array, arr);
// [{…}, {…}, {…}]
// [666, {…}, {…}]
  • 此時我們並沒有任何返回值,那麼如果有返回值的話會怎樣呢?
const array = [{value: 0}, {value: 1}, {value: 2}];
const producer = (state, fn) => produce(fn)(state);
const arr = producer(array, draft => [666, ...draft]);

console.log(array, arr);
// [{…}, {…}, {…}]
// [666, {…}, {…}, {…}]

我們發現返回值就是新數據的結果!所以我們可以清楚的得知:在沒有返回值時數據是根據函數體內對draft參數的操作生成的。有返回值的話返回值就會被當做新數據來返回。

使用use-immer來替代你的useState

由於React Hooks的異軍突起,導致現在很多組件都使用函數來進行編寫,數據就直接寫在useState中,但是有了useImmer,你以後就可以用它來代替useState啦!
還是老規矩,先安裝:

npm install immer use-immer

yarn add immer use-immer

用法

定義數據: const [xxx, setXxx] = useImmer(…)
修改數據: setXxx(draft => {})

可以看到用法和setState幾乎沒啥太大區別,接下來我們通過一個小案例來繼續深入useImmer的用法:

import React from "react";
import { useImmer } from "use-immer";


export default function () {
  const [person, setPerson] = useImmer({
    name: "馬雲",
    salary: '對錢沒興趣'
  });

  function setName(name) {
    setPerson(draft => {
      draft.name = name;
    });
  }

  function becomeRicher() {
    setPerson(draft => {
      draft.salary += '$¥';
    });
  }

  return (
    <div className="App">
      <h1>
        {person.name} ({person.salary})
      </h1>
      <input
        onChange={e => {
          setName(e.target.value);
        }}
        value={person.name}
      />
      <br />
      <button onClick={becomeRicher}>變富</button>
    </div>
  );
}


這是一個改編自官網的小例子,可以看得出useImmer的用法和useState十分相似,在保持住了簡潔性的同時還具備了immutable的數據結構,十分便捷。

useImmerReducer

use-immer對useReducer進行了加強封裝,同樣也幾乎沒什麼學習成本,再改編一下官網小案例👇

import React from "react";
import { useImmerReducer } from "use-immer";

const initialState = { salary: 0 };

function reducer(draft, action) {
  switch (action.type) {
    case "reset":
      return initialState;
    case "increment":
      return void draft.salary++;
    case "decrement":
      return void draft.salary--;
  }
}

export default function () {
  const [state, dispatch] = useImmerReducer(reducer, initialState);
  return (
    <>
      期待工資: {state.salary}K
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "reset" })}>重置</button>
    </>
  );
}


怎麼樣?看完之後是不是感覺神清氣爽,有這麼一個東西輕量、簡潔、易用又好學,看一篇文章的功夫就能學會,而且還能很好的解決你的React性能問題,那還等什麼?趕緊npm install下載安裝吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章