一 、前言
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 功能教程的资料请关注本站其它相关文章!
声明:本站所有文章 ,如无特殊说明或标注 ,均为本站原创发布。任何个人或组织 ,在未征得本站同意时 ,禁止复制、盗用 、采集 、发布本站内容到任何网站、书籍等各类媒体平台 。如若本站内容侵犯了原著者的合法权益 ,可联系我们进行处理 。