immer.js 實戰講解文件

小賊先生_ronffy發表於2018-12-05

文章在 github 開源, 歡迎 Fork 、Star !

前言

Immer 是 mobx 的作者寫的一個 immutable 庫,核心實現是利用 ES6 的 proxy,幾乎以最小的成本實現了 js 的不可變資料結構,簡單易用、體量小巧、設計巧妙,滿足了我們對JS不可變資料結構的需求。
無奈網路上完善的文件實在太少,所以自己寫了一份,本篇文章以貼近實戰的思路和流程,對 Immer 進行了全面的講解。

資料處理存在的問題

先定義一個初始物件,供後面例子使用:首先定義一個currentState物件,後面的例子使用到變數currentState時,如無特殊宣告,都是指這個currentState物件

let currentState = { 
p: {
x: [2],
},
}複製程式碼

哪些情況會一不小心修改原始物件?

// Q1let o1 = currentState;
o1.p = 1;
// currentState 被修改了o1.p.x = 1;
// currentState 被修改了// Q2fn(currentState);
// currentState 被修改了function fn(o) {
o.p1 = 1;
return o;

};
// Q3let o3 = {
...currentState
};
o3.p.x = 1;
// currentState 被修改了// Q4let o4 = currentState;
o4.p.x.push(1);
// currentState 被修改了複製程式碼

解決引用型別物件被修改的辦法

  1. 深度拷貝,但是深拷貝的成本較高,會影響效能;
  2. ImmutableJS,非常棒的一個不可變資料結構的庫,可以解決上面的問題,But,跟 Immer 比起來,ImmutableJS 有兩個較大的不足:
  • 需要使用者學習它的資料結構操作方式,沒有 Immer 提供的使用原生物件的操作方式簡單、易用;
  • 它的操作結果需要通過toJS方法才能得到原生物件,這使得在操作一個物件的時候,時刻要注意操作的是原生物件還是 ImmutableJS 的返回結果,稍不注意,就會產生意想不到的 bug。

看來目前已知的解決方案,我們都不甚滿意,那麼 Immer 又有什麼高明之處呢?

immer功能介紹

安裝immer

慾善其事必先利其器,安裝 Immer 是當前第一要務

npm i --save immer複製程式碼

immer如何fix掉那些不爽的問題

Fix Q1、Q3

import produce from 'immer';
let o1 = produce(currentState, draft =>
{
draft.p.x = 1;

})複製程式碼

Fix Q2

import produce from 'immer';
fn(currentState);
function fn(o) {
return produce(o, draft =>
{
draft.p1 = 1;

})
};
複製程式碼

Fix Q4

import produce from 'immer';
let o4 = produce(currentState, draft =>
{
draft.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 的函式

常用api介紹

使用 Immer 前,請確認將immer包引入到模組中

import produce from 'immer'複製程式碼

or

import { 
produce
} from 'immer'複製程式碼

這兩種引用方式,produce 是完全相同的

produce

備註:出現PatchListener先行跳過,後面章節會做介紹

第1種使用方式:

語法:
produce(currentState, recipe: (draftState) =>
void | draftState, ?PatchListener): nextState

例子1:

let nextState = produce(currentState, (draft) =>
{
})currentState === nextState;
// true複製程式碼

例子2:

let currentState = { 
a: [], p: {
x: 1
}
}let nextState = produce(currentState, (draft) =>
{
draft.a.push(2);

})currentState.a === nextState.a;
// falsecurrentState.p === nextState.p;
// true複製程式碼

由此可見,對 draftState 的修改都會反應到 nextState 上,而 Immer 使用的結構是共享的,nextState 在結構上又與 currentState 共享未修改的部分,共享效果如圖(借用的一篇 Immutable 文章中的動圖,侵刪):

immer.js 實戰講解文件
自動凍結功能

Immer 還在內部做了一件很巧妙的事情,那就是通過 produce 生成的 nextState 是被凍結(freeze)的,(Immer 內部使用Object.freeze方法,只凍結 nextState 跟 currentState 相比修改的部分),這樣,當直接修改 nextState 時,將會報錯。這使得 nextState 成為了真正的不可變資料。

例子:

let nextState = produce(currentState, (draft) =>
{
draft.p.x.push(2);

})currentState === nextState;
// true複製程式碼
第2種使用方式

利用高階函式的特點,提前生成一個生產者 producer

語法:
produce(recipe: (draftState) =>
void | draftState, ?PatchListener)(currentState): nextState

例子:

let producer = produce((draft) =>
{
draft.x = 2
});
let nextState = producer(currentState);
複製程式碼
recipe的返回值

recipe 是否有返回值,nextState 的生成過程是不同的:
recipe 沒有返回值時:nextState 是根據 recipe 函式內的 draftState 生成的;
recipe 有返回值時:nextState 是根據 recipe 函式的返回值生成的;

let nextState = produce(  currentState,   (draftState) =>
{
return {
x: 2
}
})複製程式碼

此時,nextState 不再是通過 draftState 生成的了,而是通過 recipe 的返回值生成的。

recipe中的this

recipe 函式內部的this指向 draftState ,也就是修改this與修改 recipe 的引數 draftState ,效果是一樣的。
注意:此處的 recipe 函式不能是箭頭函式,如果是箭頭函式,this就無法指向 draftState 了

produce(currentState, function(draft){ 
// 此處,this 指向 draftState draft === this;
// true
})複製程式碼

patch補丁功能

通過此功能,可以方便進行詳細的程式碼除錯和跟蹤,可以知道 recipe 內的做的每次修改,還可以實現時間旅行。

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, draft =>
{
draft.x = 2;
draft.y = 2;

}, (patches, inversePatches) =>
{
replaces = patches.filter(patch =>
patch.op === 'replace');
inverseReplaces = inversePatches.filter(patch =>
patch.op === 'replace');

})state = produce(state, draft =>
{
draft.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, draft =>
{
draft.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次列印結果分別是:3、2、4、1,實現了時間旅行,可以分別列印patchesinversePatches看下,

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 專案的開發效率。

用immer優化react專案的探索

首先定義一個state物件,後面的例子使用到變數state或訪問this.state時,如無特殊宣告,都是指這個state物件

state = { 
members: [ {
name: 'ronffy', age: 30
} ]
}複製程式碼

丟擲需求

就上面定義的state,我們先拋一個需求出來,好讓後面的講解有的放矢:
members 成員中的第1個成員,年齡增加1歲

優化setState方法

錯誤示例

this.state.members[0].age++;
複製程式碼

只所以有的新手同學會犯這樣的錯誤,很大原因是這樣操作實在是太方便了,以至於忘記了操作 State 的規則。

下面看下正確的實現方法

setState的第1種實現方法

const { 
members
} = this.state;
this.setState({
members: [ {
...members[0], age: members[0].age + 1,
}, ...members.slice(1), ]
})複製程式碼

setState的第2種實現方法

this.setState(state =>
{
const {
members
} = state;
return {
members: [ {
...members[0], age: members[0].age + 1,
}, ...members.slice(1) ]
}
})複製程式碼

以上2種實現方式,就是setState的兩種使用方法,相比大家都不陌生了,所以就不過多說明了,接下來看下,如果用 Immer 解決,會有怎樣的煙火?

用immer更新state

this.setState(produce(draft =>
{
draft.members[0].age++;

}))複製程式碼

是不是瞬間程式碼量就少了很多,閱讀起來舒服了很多,而且更易於閱讀了。

優化reducer

immer的produce的擴充用法

在開始正式探索之前,我們先來看下 produce 第2種使用方式的擴充用法:

例子:

let obj = {
};
let producer = produce((draft, arg) =>
{
obj === arg;
// true
});
let nextState = producer(currentState, obj);
複製程式碼

相比 produce 第2種使用方式的例子,多定義了一個obj物件,並將其作為 producer 方法的第2個引數傳了進去;可以看到, produce 內的 recipe 回撥函式的第2個引數與obj物件是指向同一塊記憶體。
ok,我們在知道了 produce 的這種擴充用法後,看看能夠在 Redux 中發揮什麼功效?

普通reducer怎樣解決上面丟擲的需求

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
}
}複製程式碼

集合immer,reducer可以怎樣寫

const reducer = (state, action) =>
produce(state, draft =>
{
switch (action.type) {
case 'ADD_AGE': draft.members[0].age++;

}
})複製程式碼

可以看到,通過 produce ,我們的程式碼量已經精簡了很多;
不過仔細觀察不難發現,利用 produce 能夠先製造出 producer 的特點,程式碼還能更優雅:

const reducer = produce((draft, action) =>
{
switch (action.type) {
case 'ADD_AGE': draft.members[0].age++;

}
})複製程式碼

好了,至此,Immer 優化 reducer 的方法也講解完畢。

Immer 的使用非常靈活,多多思考,相信你還可以發現 Immer 更多其他的妙用!

參考文件

來源:https://juejin.im/post/5c079f9b518825689f1b4e88

相關文章