<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Immer 是 mobx 的作者寫的一個 immutable 庫,核心實現是利用 ES6 的 proxy,幾乎以最小的成本實現了 js 的不可變資料結構,簡單易用、體量小巧、設計巧妙,滿足了我們對 JS 不可變資料結構的需求。
閱讀這篇文章需要以下知識儲備:
在 js 中,處理資料一直存在一個問題:
拷貝一個值的時候,如果這個值是參照型別(比如物件、陣列),直接賦值給另一個變數的時候,會把值的參照也拷貝過去,在修改新變數的過程中,舊的變數也會被一起修改掉。
要解決這個問題,通常我們不會直接賦值,而是會選擇使用深拷貝,比如JSON.parse(JSON.stringify())
,再比如 lodash
為我們提供的 cloneDeep
方法……
但是,深拷貝並不是十全十美的。
這個時候,immer
誕生了!
基本思想是,使用 Immer,會將所有更改應用到臨時 draft,它是 currentState 的代理。一旦你完成了所有的 mutations,Immer 將根據對 draft state 的 mutations 生成 nextState。這意味著你可以通過簡單地修改資料來與資料互動,同時保留不可變資料的所有好處。
const baseState = [ { title: 'Learn TypeScript', done: true, }, { title: 'Try Immer', done: false, }, ];
假設我們有上述基本狀態,我們需要更新第二個 todo,並新增第三個。但是,我們不想改變原始的 baseState,我們也想避免深度克隆以保留第一個 todo
如果沒有 Immer,我們將不得不小心地淺拷貝每層受我們更改影響的 state 結構
const nextState = [...baseState]; // 淺拷貝陣列 nextState[1] = { // 替換第一層元素 ...nextState[1], // 淺拷貝第一層元素 done: true, // 期望的更新 }; // 因為 nextState 是新拷貝的, 所以使用 push 方法是安全的, // 但是在未來的任意時間做相同的事情會違反不變性原則並且導致 bug! nextState.push({ title: 'Tweet about it' });
使用 Immer,這個過程更加簡單。我們可以利用 produce
函數,它將我們要更改的 state 作為第一個引數,對於第二個引數,我們傳遞一個名為 recipe 的函數,該函數傳遞一個 draft
引數,我們可以對其應用直接的 mutations
。一旦 recipe
執行完成,這些 mutations
被記錄並用於產生下一個狀態。 produce
將負責所有必要的複製,並通過凍結資料來防止未來的意外修改。
import produce from 'immer'; const nextState = produce(baseState, draft => { draft[1].done = true; draft.push({ title: 'Tweet about it' }); });
使用 Immer 就像擁有一個私人助理。助手拿一封信(當前狀態)並給您一份副本(草稿)以記錄更改。完成後,助手將接受您的草稿併為您生成真正不變的最終信件(下一個狀態)。
如果有一個層級很深的物件,你在使用 redux 的時候,想在 reducer 中修改它的某個屬性,但是根據 reduce 的原則,我們不能直接修改 state,而是必須返回一個新的 state
const someReducer = (state, action) => { return { ...state, first: { ...state.first, second: { ...state.first.second, third: { ...state.first.second.third, value: action, }, }, }, }; };
const someReducer = (state, action) => { state.first.second.third.value = action; };
在 Immer 之前,使用不可變資料意味著學習所有不可變的更新模式。
為了幫助“忘記”這些模式,這裡概述瞭如何利用內建 JavaScript API 來更新物件和集合
import produce from 'immer'; const todosObj = { id1: { done: false, body: 'Take out the trash' }, id2: { done: false, body: 'Check Email' }, }; // 新增 const addedTodosObj = produce(todosObj, draft => { draft['id3'] = { done: false, body: 'Buy bananas' }; }); // 刪除 const deletedTodosObj = produce(todosObj, draft => { delete draft['id1']; }); // 更新 const updatedTodosObj = produce(todosObj, draft => { draft['id1'].done = true; });
import produce from 'immer'; const todosArray = [ { id: 'id1', done: false, body: 'Take out the trash' }, { id: 'id2', done: false, body: 'Check Email' }, ]; // 新增 const addedTodosArray = produce(todosArray, draft => { draft.push({ id: 'id3', done: false, body: 'Buy bananas' }); }); // 索引刪除 const deletedTodosArray = produce(todosArray, draft => { draft.splice(3 /*索引 */, 1); }); // 索引更新 const updatedTodosArray = produce(todosArray, draft => { draft[3].done = true; }); // 索引插入 const updatedTodosArray = produce(todosArray, draft => { draft.splice(3, 0, { id: 'id3', done: false, body: 'Buy bananas' }); }); // 刪除最後一個元素 const updatedTodosArray = produce(todosArray, draft => { draft.pop(); }); // 刪除第一個元素 const updatedTodosArray = produce(todosArray, draft => { draft.shift(); }); // 陣列開頭新增元素 const addedTodosArray = produce(todosArray, draft => { draft.unshift({ id: 'id3', done: false, body: 'Buy bananas' }); }); // 根據 id 刪除 const deletedTodosArray = produce(todosArray, draft => { const index = draft.findIndex(todo => todo.id === 'id1'); if (index !== -1) { draft.splice(index, 1); } }); // 根據 id 更新 const updatedTodosArray = produce(todosArray, draft => { const index = draft.findIndex(todo => todo.id === 'id1'); if (index !== -1) { draft[index].done = true; } }); // 過濾 const updatedTodosArray = produce(todosArray, draft => { // 過濾器實際上會返回一個不可變的狀態,但是如果過濾器不是處於物件的頂層,這個依然很有用 return draft.filter(todo => todo.done); });
import produce from 'immer'; // 複雜資料結構例子 const store = { users: new Map([ [ '17', { name: 'Michel', todos: [{ title: 'Get coffee', done: false }], }, ], ]), }; // 深度更新 const nextStore = produce(store, draft => { draft.users.get('17').todos[0].done = true; }); // 過濾 const nextStore = produce(store, draft => { const user = draft.users.get('17'); user.todos = user.todos.filter(todo => todo.done); });
允許從 recipe 返回 Promise 物件。或者使用 async / await。這對於長時間執行的程序非常有用,只有在 Promise 鏈解析後才生成新物件
注意,如果 producer 是非同步的,produce 本身也會返回一個 promise。
例子:
import produce from 'immer'; const user = { name: 'michel', todos: [] }; const loadedUser = await produce(user, async draft => { draft.todos = await (await fetch('http://host/' + draft.name)).json(); });
請注意,draft 不應從非同步程式中“洩露”並儲存在其他地方。非同步過程完成後,draft 仍將被釋放
createDraft
和 finishDraft
是兩個底層函數,它們對於在 immer 之上構建抽象的庫非常有用。避免了為了使用 draft 始終建立函數。
相反,人們可以建立一個 draft,對其進行修改,並在未來的某個時間完成該 draft,在這種情況下,將產生下一個不可變狀態。
例如,我們可以將上面的範例重寫為:
import { createDraft, finishDraft } from 'immer'; const user = { name: 'michel', todos: [] }; const draft = createDraft(user); draft.todos = await (await fetch('http://host/' + draft.name)).json(); const loadedUser = finishDraft(draft);
當向 Immer producer 中的狀態樹新增大型資料集時(例如從 JSON 端接收的資料),可以在首先新增的資料的最外層呼叫 freeze(json)
來淺凍結它。這將允許 Immer 更快地將新資料新增到樹中,因為它將避免遞迴掃描和凍結新資料的需要。
immer 在任何地方都是可選的,因此手動編寫效能非常苛刻的 reducers ,並將 immer 用於所有普通的的 reducers 是非常好的。即使在 producer 內部,您也可以通過使用 original
或 current
函數來選擇退出 Immer 的某些部分邏輯,並對純 JavaScript 物件執行一些操作。
Immer 會將您在 draft 中讀取的任何內容也遞迴地轉換為 draft。如果您對涉及大量讀取操作的 draft 進行昂貴的無副作用操作,例如在非常大的陣列中使用 find(Index)
查詢索引,您可以通過首先進行搜尋,並且只在知道索引後呼叫 produce
來加快速度。這樣可以阻止 Immer 將在 draft 中搜尋到的所有內容都進行轉換。或者,使用 original(someDraft)
對 draft 的原始值執行搜尋,這歸結為同樣的事情。
始終嘗試將 produce “向上”拉動,例如 for (let x of y) produce(base, d => d.push(x))
比 produce(base, d => { for (let x of y) ) d.push(x)})
慢得多
永遠不要重新分配 draft
引數(例如:draft = myNewState
)。相反,要麼修改 draft,要麼返回新狀態。
Immer 假設您的狀態是單向樹。也就是說,任何物件都不應該在樹中出現兩次,也不應該有迴圈參照。從根到樹的任何節點應該只有一條路徑。
undefined
可以從 producers 返回值,但不能以這種方式返回 undefined
,因為它與根本不更新 draft 沒有區別!
Immer 不支援特殊物件 比如 window.location
對於陣列,只能改變數值屬性和 length 屬性。自定義屬性不會保留在陣列上。
請注意,來自閉包而不是來自基本 state 的資料將永遠不會被 draft,即使資料已成為新 darft 的一部分
const onReceiveTodo = todo => { const nextTodos = produce(todos, draft => { draft.todos[todo.id] = todo; // 注意,因為 todo 來自外部,而不是 draft,所以他不會被 draft, // 所以下面的修改會影響原來的 todo! draft.todos[todo.id].done = true; // 上面的程式碼相當於 todo.done = true; draft.todos[todo.id] = todo; }); };
支援巢狀呼叫 produce
,但請注意 produce
將始終產生新狀態,因此即使將 draft 傳遞給巢狀 produce,內部 produce 所做的更改也不會在傳遞給它的 draft 中可見,只會反映在產生的輸出中。
換句話說,當使用巢狀 produce 時,您會得到 draft 的 draft,並且內部 produce 的結果會被合併回原始 draft(或返回)
// 巢狀的錯誤寫法: produce(state, draft => { produce(draft.user, userDraft => { userDraft.name += '!'; }); });
// 巢狀的正確寫法: produce(state, draft => { draft.user = produce(draft.user, userDraft => { userDraft.name += '!'; }); });
Immer 中的 draft 物件包裝在 Proxy
中,因此您不能使用 == 或 === 來測試原始物件與其 draft 之間的相等性,相反,可以使用 original:
const remove = produce((list, element) => { const index = list.indexOf(element); // 不會工作! const index = original(list).indexOf(element); // 用這個! if (index !== -1) { list.splice(index, 1); } }); const values = [a, b, c]; remove(values, a);
如果可以的話,建議在 produce
函數之外執行比較,或者使用 .id
之類的唯一識別符號屬性,以避免需要使用 original
。
以上就是Immer 功能最佳實踐範例教學的詳細內容,更多關於Immer 功能教學的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45