文章在 github 開源, 歡迎 Fork 、Star !javascript
Immer 是 mobx 的做者寫的一個 immutable 庫,核心實現是利用 ES6 的 proxy,幾乎以最小的成本實現了 js 的不可變數據結構,簡單易用、體量小巧、設計巧妙,知足了咱們對JS不可變數據結構的需求。
無奈網絡上完善的文檔實在太少,因此本身寫了一份,本篇文章以貼近實戰的思路和流程,對 Immer 進行了全面的講解。java
先定義一個初始對象,供後面例子使用: 首先定義一個currentState
對象,後面的例子使用到變量currentState
時,如無特殊聲明,都是指這個currentState
對象react
let currentState = {
p: {
x: [2],
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/8/17293174bf805c43~tplv-t2oaga2asx-image.image)
},
}
複製代碼
哪些狀況會一不當心修改原始對象?git
// Q1
let o1 = currentState;
o1.p = 1; // currentState 被修改了
o1.p.x = 1; // currentState 被修改了
// Q2
fn(currentState); // currentState 被修改了
function fn(o) {
o.p1 = 1;
return o;
};
// Q3
let o3 = {
...currentState
};
o3.p.x = 1; // currentState 被修改了
// Q4
let o4 = currentState;
o4.p.x.push(1); // currentState 被修改了
複製代碼
toJS
方法才能獲得原生對象,這使得在操做一個對象的時候,時刻要注意操做的是原生對象仍是 ImmutableJS 的返回結果,稍不注意,就會產生意想不到的 bug。看來目前已知的解決方案,咱們都不甚滿意,那麼 Immer 又有什麼高明之處呢?github
慾善其事必先利其器,安裝 Immer 是當前第一要務typescript
npm i --save immer
複製代碼
Fix Q一、Q3shell
import produce from 'immer';
let o1 = produce(currentState, draftState => {
draftState.p.x = 1;
})
複製代碼
Fix Q2npm
import produce from 'immer';
fn(currentState);
function fn(o) {
return produce(o, draftState => {
draftState.p1 = 1;
})
};
複製代碼
Fix Q4api
import produce from 'immer';
let o4 = produce(currentState, draftState => {
draftState.p.x.push(1);
})
複製代碼
是否是使用很是簡單,經過小試牛刀,咱們簡單的瞭解了 Immer ,下面將對 Immer 的經常使用 api 分別進行介紹。數組
Immer 涉及概念很少,在此將涉及到的概念先行羅列出來,閱讀本文章過程當中遇到不明白的概念,能夠隨時來此處查閱。
currentState
被操做對象的最初狀態
draftState
根據 currentState 生成的草稿狀態,它是 currentState 的代理,對 draftState 所作的任何修改都將被記錄並用於生成 nextState 。在此過程當中,currentState 將不受影響
nextState
根據 draftState 生成的最終狀態
produce 生產
用來生成 nextState 或 producer 的函數
producer 生產者
經過 produce 生成,用來生產 nextState ,每次執行相同的操做
recipe 生產機器
用來操做 draftState 的函數
使用 Immer 前,請確認將immer
包引入到模塊中
import produce from 'immer'
複製代碼
or
import { produce } from 'immer'
複製代碼
這兩種引用方式,produce 是徹底相同的
備註:出現PatchListener
先行跳過,後面章節會作介紹
語法:
produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState
例子1:
let nextState = produce(currentState, (draftState) => {
})
currentState === nextState; // true
複製代碼
例子2:
let currentState = {
a: [],
p: {
x: 1
}
}
let nextState = produce(currentState, (draftState) => {
draftState.a.push(2);
})
currentState === nextState // false
currentState.a === nextState.a; // false
currentState.p === nextState.p; // true
複製代碼
因而可知,對 draftState 的修改都會反映到 nextState 上。而 Immer 使用的結構是共享的,nextState 在結構上與 currentState 共享未修改的部分,共享效果如圖(借用的一篇 Immutable 文章中的動圖,侵刪):
Immer 還在內部作了一件很巧妙的事情,那就是經過 produce 生成的 nextState 是被凍結(freeze)的,(Immer 內部使用Object.freeze
方法,只凍結 nextState 跟 currentState 相比修改的部分),這樣,當直接修改 nextState 時,將會報錯。 這使得 nextState 成爲了真正的不可變數據。
示例:
const currentState = {
p: {
x: [2],
},
};
const nextState = produce(currentState, draftState => {
draftState.p.x.push(3);
});
console.log(nextState.p.x); // [2, 3]
nextState.p.x = 4;
console.log(nextState.p.x); // [2, 3]
nextState.p.x.push(5); // 報錯
複製代碼
利用高階函數的特色,生成一個生產者 producer
語法:
produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState
例子:
let producer = produce((draftState) => {
draftState.x = 2
});
let nextState = producer(currentState);
複製代碼
recipe 是否有返回值,nextState 的生成過程不一樣:
recipe 沒有返回值時:nextState 根據 draftState 生成;
recipe 有返回值時:nextState 根據 recipe 函數的返回值生成;
let nextState = produce(currentState, (draftState) => {
return {
x: 5
}
}
)
console.log(nextState); // {x: 5}
複製代碼
此時,nextState 再也不是經過 draftState 生成,而是經過 recipe 的返回值生成。
recipe 函數內部的this
指向 draftState ,也就是修改this
與修改 recipe 的參數 draftState ,效果是同樣的。
注意:此處的 recipe 函數不能是箭頭函數,若是是箭頭函數,this
就沒法指向 draftState 了
produce(currentState, function(draftState){
// 此處,this 指向 draftState
draftState === this; // true
})
複製代碼
經過此功能,能夠方便進行詳細的代碼調試和跟蹤,能夠知道 draftState 的每次修改,還能夠實現時間旅行。
Immer 中,一個 patch 對象以下:
interface Patch {
op: "replace" | "remove" | "add" // 一次更改的動做類型
path: (string | number)[] // 此屬性指從樹根到被更改樹杈的路徑
value?: any // op爲 replace、add 時,纔有此屬性,表示新的賦值
}
複製代碼
語法:
produce(
currentState,
recipe,
// 經過 patchListener 函數,暴露正向和反向的補丁數組
patchListener: (patches: Patch[], inversePatches: Patch[]) => void
)
applyPatches(currentState, changes: (patches | inversePatches)[]): nextState
複製代碼
例子:
import produce, { applyPatches } from "immer"
let state = {
x: 1
}
let replaces = [];
let inverseReplaces = [];
state = produce(
state,
draftState => {
draftState.x = 2;
draftState.y = 2;
},
(patches, inversePatches) => {
replaces = patches.filter(patch => patch.op === 'replace');
inverseReplaces = inversePatches.filter(patch => patch.op === 'replace');
}
)
state = produce(state, draftState => {
draftState.x = 3;
})
console.log('state1', state); // { x: 3, y: 2 }
state = applyPatches(state, replaces);
console.log('state2', state); // { x: 2, y: 2 }
state = produce(state, draftState => {
draftState.x = 4;
})
console.log('state3', state); // { x: 4, y: 2 }
state = applyPatches(state, inverseReplaces);
console.log('state4', state); // { x: 1, y: 2 }
複製代碼
state.x
的值4次打印結果分別是:三、二、四、1
,實現了時間旅行, 能夠分別打印patches
和inversePatches
看下,
patches
數據以下:
[
{
op: "replace",
path: ["x"],
value: 2
},
{
op: "add",
path: ["y"],
value: 2
},
]
複製代碼
inversePatches
數據以下:
[
{
op: "replace",
path: ["x"],
value: 1
},
{
op: "remove",
path: ["y"],
},
]
複製代碼
可見,patchListener
內部對數據操做作了記錄,並分別存儲爲正向操做記錄和反向操做記錄,供咱們使用。
至此,Immer 的經常使用功能和 api 咱們就介紹完了。
接下來,咱們看如何用 Immer ,提升 React 、Redux 項目的開發效率。
首先定義一個state
對象,後面的例子使用到變量state
或訪問this.state
時,如無特殊聲明,都是指這個state
對象
state = {
members: [
{
name: 'ronffy',
age: 30
}
]
}
複製代碼
就上面定義的state
,咱們先拋一個需求出來,好讓後面的講解有的放矢:
members 成員中的第1個成員,年齡增長1歲
this.state.members[0].age++;
複製代碼
只因此有的新手同窗會犯這樣的錯誤,很大緣由是這樣操做實在是太方便了,以致於忘記了操做 state 的規則。
下面看下正確的實現方法
const { members } = this.state;
this.setState({
members: [
{
...members[0],
age: members[0].age + 1,
},
...members.slice(1),
]
})
複製代碼
this.setState(state => {
const { members } = state;
return {
members: [
{
...members[0],
age: members[0].age + 1,
},
...members.slice(1)
]
}
})
複製代碼
以上2種實現方式,就是setState
的兩種使用方法,想必你們都不陌生。接下來看下,若是用 Immer 解決,會有怎樣的煙火?
this.setState(produce(draftState => {
draftState.members[0].age++;
}))
複製代碼
是否是馬上代碼量就少了不少,並且更易於閱讀。
在開始正式探索以前,咱們先來看下 produce 第2種使用方式的拓展用法:
例子:
let obj = {};
let producer = produce((draftState, arg) => {
obj === arg; // true
});
let nextState = producer(currentState, obj);
複製代碼
相比 produce 第2種使用方式的例子,多定義了一個obj
對象,並將其做爲 producer 方法的第2個參數傳了進去;能夠看到, produce 內的 recipe 回調函數的第2個參數與obj
對象是指向同一塊內存。
ok,咱們在知道了 produce 的這種拓展用法後,看看可以在 Redux 中發揮什麼功效?
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_AGE':
const { members } = state;
return {
...state,
members: [
{
...members[0],
age: members[0].age + 1,
},
...members.slice(1),
]
}
default:
return state
}
}
複製代碼
const reducer = (state, action) => produce(state, draftState => {
switch (action.type) {
case 'ADD_AGE':
draftState.members[0].age++;
}
})
複製代碼
能夠看到,經過 produce ,咱們的代碼量已經精簡了不少;
不過,仔細觀察不難發現,利用 produce 可以製造 producer 的特色,代碼還能更優雅:
const reducer = produce((draftState, action) => {
switch (action.type) {
case 'ADD_AGE':
draftState.members[0].age++;
}
})
複製代碼
好了,至此,Immer 優化 reducer 的方法也講解完畢。
Immer 的使用很是靈活,並且還有一些其餘拓展 api ,多多研究,相信你還能夠發現 Immer 更多其餘的妙用!