摘要:無奈網絡上完善的文檔實在太少,所以自己寫了一份,本篇文章以貼近實戰的思路和流程,對進行了全面的講解。這使得成為了真正的不可變數據。的使用非常靈活,多多思考,相信你還可以發現更多其他的妙用參考文檔官方文檔
文章在 github 開源, 歡迎 Fork 、Star前言
Immer 是 mobx 的作者寫的一個 immutable 庫,核心實現是利用 ES6 的 proxy,幾乎以最小的成本實現了 js 的不可變數據結構,簡單易用、體量小巧、設計巧妙,滿足了我們對JS不可變數據結構的需求。
無奈網絡上完善的文檔實在太少,所以自己寫了一份,本篇文章以貼近實戰的思路和流程,對 Immer 進行了全面的講解。
先定義一個初始對象,供后面例子使用:
首先定義一個currentState對象,后面的例子使用到變量currentState時,如無特殊聲明,都是指這個currentState對象
let currentState = { p: { x: [2], }, }
哪些情況會一不小心修改原始對象?
// 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 被修改了解決引用類型對象被修改的辦法
深度拷貝,但是深拷貝的成本較高,會影響性能;
ImmutableJS,非常棒的一個不可變數據結構的庫,可以解決上面的問題,But,跟 Immer 比起來,ImmutableJS 有兩個較大的不足:
需要使用者學習它的數據結構操作方式,沒有 Immer 提供的使用原生對象的操作方式簡單、易用;
它的操作結果需要通過toJS方法才能得到原生對象,這使得在操作一個對象的時候,時刻要注意操作的是原生對象還是 ImmutableJS 的返回結果,稍不注意,就會產生意想不到的 bug。
看來目前已知的解決方案,我們都不甚滿意,那么 Immer 又有什么高明之處呢?
immer功能介紹 安裝immer欲善其事必先利其器,安裝 Immer 是當前第一要務
npm i --save immerimmer如何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 的函數
使用 Immer 前,請確認將immer包引入到模塊中
import produce from "immer"
or
import { produce } from "immer"
這兩種引用方式,produce 是完全相同的
produce備注:出現PatchListener先行跳過,后面章節會做介紹
語法:
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; // false currentState.p === nextState.p; // true
由此可見,對 draftState 的修改都會反應到 nextState 上,而 Immer 使用的結構是共享的,nextState 在結構上又與 currentState 共享未修改的部分,共享效果如圖(借用的一篇 Immutable 文章中的動圖,侵刪):
Immer 還在內部做了一件很巧妙的事情,那就是通過 produce 生成的 nextState 是被凍結(freeze)的,(Immer 內部使用Object.freeze方法,只凍結 nextState 跟 currentState 相比修改的部分),這樣,當直接修改 nextState 時,將會報錯。
這使得 nextState 成為了真正的不可變數據。
例子:
let nextState = produce(currentState, (draft) => { draft.p.x.push(2); }) currentState === nextState; // true
利用高階函數的特點,提前生成一個生產者 producer
語法:
produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState
例子:
let producer = produce((draft) => { draft.x = 2 }); let nextState = producer(currentState);
recipe 是否有返回值,nextState 的生成過程是不同的:
recipe 沒有返回值時:nextState 是根據 recipe 函數內的 draftState 生成的;
recipe 有返回值時:nextState 是根據 recipe 函數的返回值生成的;
let nextState = produce( currentState, (draftState) => { return { x: 2 } } )
此時,nextState 不再是通過 draftState 生成的了,而是通過 recipe 的返回值生成的。
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,實現了時間旅行,
可以分別打印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 項目的開發效率。
用immer優化react項目的探索首先定義一個state對象,后面的例子使用到變量state或訪問this.state時,如無特殊聲明,都是指這個state對象
state = { members: [ { name: "ronffy", age: 30 } ] }拋出需求
就上面定義的state,我們先拋一個需求出來,好讓后面的講解有的放矢:
members 成員中的第1個成員,年齡增加1歲
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更新statethis.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 中發揮什么功效?
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 更多其他的妙用!
參考文檔官方文檔
Introducing Immer: Immutability the easy way
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/99761.html
摘要:例如維護一份在內部,來判斷是否有變化,下面這個例子就是一個構造函數,如果將它的實例傳入對象作為第一個參數,就能夠后面的處理對象中使用其中的方法上面這個構造函數相比源代碼省略了很多判斷的部分。 showImg(https://segmentfault.com/img/bV27Dy?w=1400&h=544); 博客鏈接:下一代狀態管理工具 immer 簡介及源碼解析 JS 里面的變量類...
摘要:所以整個過程只涉及三個輸入狀態,中間狀態,輸出狀態關鍵是是如何生成,如何應用修改,如何生成最終的。至此基本把上的模式解析完畢。結束實現還是相當巧妙的,以后可以在狀態管理上使用一下。 開始 在函數式編程中,Immutable這個特性是相當重要的,但是在Javascript中很明顯是沒辦法從語言層面提供支持,但是還有其他庫(例如:Immutable.js)可以提供給開發者用上這樣的特性,所...
摘要:所以整個過程只涉及三個輸入狀態,中間狀態,輸出狀態關鍵是是如何生成,如何應用修改,如何生成最終的。至此基本把上的模式解析完畢。結束實現還是相當巧妙的,以后可以在狀態管理上使用一下。 開始 在函數式編程中,Immutable這個特性是相當重要的,但是在Javascript中很明顯是沒辦法從語言層面提供支持,但是還有其他庫(例如:Immutable.js)可以提供給開發者用上這樣的特性,所...
閱讀 1539·2023-04-26 00:25
閱讀 926·2021-09-27 13:36
閱讀 936·2019-08-30 14:14
閱讀 2186·2019-08-29 17:10
閱讀 1018·2019-08-29 15:09
閱讀 1954·2019-08-28 18:21
閱讀 974·2019-08-26 13:27
閱讀 984·2019-08-26 10:58