不可變數據
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下載安裝吧!