您现在的位置是:网站首页> 编程资料编程资料
Immer 功能最佳实践示例教程_JavaScript_
2023-05-24
385人已围观
简介 Immer 功能最佳实践示例教程_JavaScript_
一、前言
Immer 是 mobx 的作者写的一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对 JS 不可变数据结构的需求。
二、学习前提
阅读这篇文章需要以下知识储备:
- JavaScript 基础语法
- es6 基础语法
- node、npm 基础知识
三、历史背景
在 js 中,处理数据一直存在一个问题:
拷贝一个值的时候,如果这个值是引用类型(比如对象、数组),直接赋值给另一个变量的时候,会把值的引用也拷贝过去,在修改新变量的过程中,旧的变量也会被一起修改掉。
要解决这个问题,通常我们不会直接赋值,而是会选择使用深拷贝,比如JSON.parse(JSON.stringify()),再比如 lodash 为我们提供的 cloneDeep 方法……
但是,深拷贝并不是十全十美的。
这个时候,immer 诞生了!
四、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
如果没有 Immer,我们将不得不小心地浅拷贝每层受我们更改影响的 state 结构
const nextState = [...baseState]; // 浅拷贝数组 nextState[1] = { // 替换第一层元素 ...nextState[1], // 浅拷贝第一层元素 done: true, // 期望的更新 }; // 因为 nextState 是新拷贝的, 所以使用 push 方法是安全的, // 但是在未来的任意时间做相同的事情会违反不变性原则并且导致 bug! nextState.push({ title: 'Tweet about it' }); 使用 Immer
使用 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
不使用 Immer
const someReducer = (state, action) => { return { ...state, first: { ...state.first, second: { ...state.first.second, third: { ...state.first.second.third, value: action, }, }, }, }; }; 使用 Immer
const someReducer = (state, action) => { state.first.second.third.value = action; }; 好处
- 遵循不可变数据范式,同时使用普通的 JavaScript 对象、数组、Sets 和 Maps。无需学习新的 API 或 "mutations patterns"!
- 强类型,无基于字符串的路径选择器等
- 开箱即用的结构共享
- 开箱即用的对象冻结
- 深度更新轻而易举
- 样板代码减少。更少的噪音,更简洁的代码
更新模式
在 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); }); 异步 producers & createDraft
允许从 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
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 对象执行一些操作。
对于性能消耗大的的搜索操作,从原始 state 读取,而不是 draft
Immer 会将您在 draft 中读取的任何内容也递归地转换为 draft。如果您对涉及大量读取操作的 draft 进行昂贵的无副作用操作,例如在非常大的数组中使用 find(Index) 查找索引,您可以通过首先进行搜索,并且只在知道索引后调用 produce 来加快速度。这样可以阻止 Immer 将在 draft 中搜索到的所有内容都进行转换。或者,使用 original(someDraft) 对 draft 的原始值执
相关内容
- react源码层探究setState作用_React_
- React中常见的TypeScript定义实战教程_React_
- Vue组件通信之父传子与子传父详细讲解_vue.js_
- JavaScript箭头函数与普通函数的区别示例详解_javascript技巧_
- antv完成区间柱形图一列多柱配置实现详解_vue.js_
- Vue如何进行数据代理_vue.js_
- Vue3系列之effect和ReactiveEffect track trigger源码解析_vue.js_
- JavaScript前后端数据交互工具ajax使用教程_javascript技巧_
- 时间处理工具 dayjs使用示例详解_javascript技巧_
- vue3渲染函数(h函数)的变更剖析_vue.js_
