首页IT科技immerit(Immer功能最佳实践示例教程)

immerit(Immer功能最佳实践示例教程)

时间2025-08-04 13:55:48分类IT科技浏览5011
导读:一、前言 Immer 是 mobx 的作者写的一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对 JS 不可变数据结构的需求。...

一               、前言

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 的原始值执行搜索                       ,这归结为同样的事情        。

将 produce 拉到尽可能远的地方

始终尝试将 produce “向上       ”拉动                ,例如 for (let x of y) produce(base, d => d.push(x)) 比 produce(base, d => { for (let x of y) ) d.push(x)}) 慢得多

六                        、陷阱

不要重新分配 recipe 参数

永远不要重新分配 draft 参数(例如:draft = myNewState)               。相反,要么修改 draft                       ,要么返回新状态                       。

Immer 只支持单向树

Immer 假设您的状态是单向树        。也就是说                        ,任何对象都不应该在树中出现两次,也不应该有循环引用        。从根到树的任何节点应该只有一条路径                       。

永远不要从 producer 那里显式返回 undefined

可以从 producers 返回值               ,但不能以这种方式返回 undefined                        ,因为它与根本不更新 draft 没有区别!

不要修改特殊对象

Immer 不支持特殊对象 比如 window.location

只有有效的索引和长度可以在数组上改变

对于数组        ,只能改变数值属性和 length 属性                。自定义属性不会保留在数组上        。

只有来自 state 的数据会被 draft

请注意               ,来自闭包而不是来自基本 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; }); };

始终使用嵌套 producers 的结果

支持嵌套调用 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 += !; }); });

Drafts 在引用上不相等

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 功能教程的资料请关注本站其它相关文章!

声明:本站所有文章                       ,如无特殊说明或标注                ,均为本站原创发布。任何个人或组织        ,在未征得本站同意时                       ,禁止复制、盗用               、采集                        、发布本站内容到任何网站        、书籍等各类媒体平台                       。如若本站内容侵犯了原著者的合法权益                ,可联系我们进行处理                        。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
网站突然被降权怎么回事(网站突然被降权怎么解决) 前端导出pdf文件插件(PDF.js 实现pdf分页加载的前端优化(实现按需加载)-前端的demo也已经上传)