React 作为当前前端最受欢迎的框架之一,极大的提升了前端开发效率。 其拥有庞大的开发者群体,其社区也非常活跃,因此围绕 React 也产出了非常多的第三方库。 Immer 就是其中之一。
一、Immer基本介绍
代码语言:javascript复制Immer (German for: always) is a tiny package that allows you to work with
immutable state in a more convenient way.
Immer 是一个很小的包,它能让您以更方便的方式处理不可变状态(immutable state)。 是 2019 年 React“年度突破”开源奖和“最具影响力贡献”JavaScript 开源奖得主。
Immer 简化了不可变数据结构的处理
Immer 可用于需要使用不可变数据结构的任何上下文中。 例如,结合 React 状态、React 或 Redux reducers 或配置管理等。
针对不可变的数据结构能够做到变更检测:
代码语言:javascript复制即如果对象的引用没有更改,则对象本身也没有更改。
(如果没有在 draft 中对 state 对象做修改,那么返回值和原对象是一样的,绝对相等)
此外,它还使得克隆成本相对较低:
代码语言:javascript复制原对象中,未更改的属性(树)部分不做复制,在内存中与原旧版本的属性共享属性(树)。
通常来说,为了不更改原对象、数组或映射的任何属性,但又需要创建新对象并对其属性进行操作的时候 我们通常是对原对象进行深拷贝,然后通过操作拷贝的对象的属性来实现。
但是,这在实践中,可能导致写出相当繁琐的代码,并且很容易意外影响到原对象。 Immer 的出现就是为了解决这些问题,它能解决如下痛点:
代码语言:javascript复制1.Immer会检测到意外变更并抛出错误。
2.Immer能避免对不可变对象进行深度更新时,所需要的常规手动拷贝代码的实现
如果没有Immer,对象副本需要在每一级上手工创建其副本,通常通过使用很解构操作(…obj)操作。
当使用Immer时,只需要对 draft对象进行更改,draft对象会先记录用户的修改,
然后仅创建有变更的必要的属性副本,不会影响原始对象。
3.在使用Immer时,您不需要额外学习专用的api或数据结构,
使用普通的JavaScript数据结构并使用常规方式修改数据即可,操作简单且安全。
为什么要使用Immer?
假如有如下 state 数组:
代码语言:javascript复制const baseState = [
{
title: 'Learn TypeScript',
done: true,
},
{
title: 'Try Immer',
done: false,
},
];
我们需要将 baseState 数组的状态进行变更,变更为一个新的 state 状态 同时,原本的 baseState 不能被修改。
代码语言:javascript复制1)对第二项数据的 done 值进行变更为 true
2)为 state 数组新增一个新的数据项```{ title: 'Tweet about it' }```。
如果不使用 Immer 我们将不得不小心翼翼地浅层复制状态结构的每一层,这将取决于我们的手工操作是否仔细。
代码语言:javascript复制const nextState = baseState.slice(); // 浅复制数组对象
// 替换数组第一项的数据
nextState[1] = {
...nextState[1], // 使用解构语法复制数组第一项的第一个元素对象
done: true, // 新增新属性,合并进入数组第一项的第一个元素对象
};
// 因为 nextState 是新克隆的,所以这里使用 push 是安全的,
// 但是在未来的其它时间,如果做同样的操作的时候就可能就会违反不可变原则,从而并引入 bug!,这需要我们非常小心
nextState.push({ title: 'Tweet about it' });
使用 Immer 使用 Immer,能让这个过程更直接。 我们可以利用 produce 函数,它的第一个参数为我们想要操作的初始的状态。 第二个参数是我们传递一个名为 recipe 的函数 该函数自动传入了一个 draft 对象作为参数,我们可以直接修改该 draft 对象。 一旦修改完成,这些修改将被记录下来并用于后续产生下一个状态。 之后,Produce 将负责将上面的变更进行必要的复制,并通对对象进行冻结,防止未来被意外修改。
实现代码如下:
代码语言:javascript复制import produce from 'immer';
const nextState = produce(baseState, (draft) => {
draft[1].done = true;
draft.push({ title: 'Tweet about it' });
});
很显然,使用 immer 之后,相比于之前手工实现简单了不少,失误的可能性更低了。 同时, produce 对其对象的冻结也避免了其在此后的操作中被意外修改的可能性。
代码语言:javascript复制Immer 就像是一个私人助理。
助理拿着一封信(当前状态),给你一份草稿纸,让你在上面写你想要做的修改。
当你写完之后,助手就会拿起你的草稿,根据草稿内容为你写出真正不能再被修改的、最后版本的书信(即下一个状态)。
Immer 的优势:
代码语言:javascript复制1. 遵循不可变数据规范,同时使用普通的JavaScript对象、数组、集合和映射。不需要学习新的api或“语法”!
2. 强类型,没有基于字符串的路径选择器等。 结构共享,仅复制需要的数据部分。
3. 冻结对象,不会被轻易改变。
4. 深度更新轻而易举,不需要人工考虑其数据结构会被影响或者遗漏。
5. 使用简单,能使代码更简洁。
6. 对JSON补丁的一流支持
7. 体积小,gzipped 压缩后仅3 kb
二、Immer的使用场景
应用的场景有:
代码语言:javascript复制1. 用于 React 的 state 的变更。
React 的 state 本身是不可修改的,当你需要修改它的某个属性然后保存为新的状态的时候,
使用 immer 可以很方便的获得一个新的 state。
2. 需要复制一个不可变对象,在不改变原对象的情况下,修改其中的某个值,保存为一个新的对象。
3. 复制一个不可变的数组,在不改变原数组的情况下,修改其中某个值,保存为新的数组。
类似于深拷贝:
代码语言:javascript复制import produce from 'immer';
const baseState = { a: 1 };
const nextState = produce(baseState, (draft) => {
draft[1].done = true;
draft.push({ title: 'Tweet about it' });
});
如上代码,baseState 为原状态,draft 可以看做是 baseState 的深拷贝对象(其实不是,它是一个代理对象)。 当然,其效果和深拷贝对象是非常类似的,和操作一个对象的完全复制体一样,修改 draft 的时候并不会影响原来的 baseState。
为什么不直接使用深拷贝呢?
上面说了,draft 既然可以看做是 baseState 的深拷贝对象,为什么不直接使用深拷贝呢? 还是有区别的,因为 immer 处理对象也仅仅是看起来像是深拷贝,其实不是,还是有一些区别的。
首先,深拷贝是完全复制,拷贝之后的对象和原对象有不同的堆内存存储空间。
代码语言:javascript复制const baseState = [{ a: { b: 1 } }, { a: 2 }];
const nextState = JSON.parse(JSON.stringify(baseState));
console.log(nextState === baseState); // false
深拷贝之后其对象的指针已经不一样了,因此拷贝之后对象和原对象并不相等。
当然,即使是浅拷贝,新旧对象也不再相等,其对象的指针也会改变。 如下代码所示:
代码语言:javascript复制const baseState = [{ a: { b: 1 } }, { a: 2 }];
const nextState = [...baseState];
console.log(nextState === baseState); // false
再来看 immer:
代码语言:javascript复制import produce from 'immer';
const baseState = [{ a: { b: 1 } }, { a: 2 }];
const nextState = produce(baseState, (draft) => {
draft[0].a.b = 1;
});
console.log(nextState === baseState); // true
可以看出,经过 immer 处理之后,两个对象竟然是相等的。 这是因为,immer 在处理 draft的时候,如果没有变更,或者变更之后和原来一样就不会改变对象,其对象指针还是同一个。
那么如果 draft 内部处理的时候有变更呢?
如下代码所示:
代码语言:javascript复制import produce from 'immer';
const baseState = [{ a: { b: 1 } }, { a: 2 }];
const nextState = produce(baseState, (draft) => {
draft[0].a.b = 2;
});
console.log(nextState === baseState); // false
console.log(nextState, baseState); // 二者的值不一样。
从打印结果就可以看出来,当 draft 修改对象属性之后,二者就不再相等了。
显然 produce 返回的 nextState 对象和原来的 baseState不一样了。
这是为什么呢? 原因就在于 Immer draft 的实现:
代码语言:javascript复制draft 是个 Proxy 代理对象,对它的读写操作会走到内部定义的 getter/setter 里。
当访问 draft 时,其定义的 getter 会返回一个 Proxy 代理对象。
如果在 draft 中没有值的变更或者变更值和原对象一致,则返回原对象。
当给 draft 设置赋值产生变更之后,setter 就会对原对象的 copy 对象进行赋值,之后再返回 copy 对象。
当然,这个返回的 copy 对象并不是原对象的完全 copy,
而只是在原对象的基础上加上了相关变更数据,然后返回这个综合对象。
Immer 仅适用于处理不可变对象
我们可以再回头看看前文中 immer 基本介绍的那一句英文,此为官网的原文。
代码语言:javascript复制that allows you to work with immutable state in a more convenient way.
with immutable state
不可变对象。
也就是说,immer 的根本目的是为了处理“不可变对象”而存在的(比如 React 的 state)。
为什么说是为了处理不可变对象呢? 对普通对象难道不行吗?
代码语言:javascript复制最好不要。
当然,也可以试试看,比如如下代码:
代码语言:javascript复制import produce from 'immer';
const baseState = [{ a: { b: 1 } }, { a: 2 }];
const nextState = produce(baseState, (draft) => {
draft[0].a.b = 2;
});
console.log(nextState); // 输出值: [{ a: { b: 2 } }, { a: 2 }]
console.log(baseState); // 输出值: [{ a: { b: 1 } }, { a: 2 }]
此时,我们来修改一下 nextState/baseState 对象的属性值:
代码语言:javascript复制// 直接修改 nextState 的属性值
nextState.a.b = 999;
代码语言:javascript复制// 直接修改 baseState 的属性值
baseState.a.b = 999;
可以看到,报错了。 很显然,经过 immer 处理之后的 nextState 修改属性值的时候报错了。 而且,原对象 baseState 修改属性值的时候同样会报错。
代码语言:javascript复制immer 改变了原对象!!!
原因也不难理解,毕竟这就是Immer的两大“亮点”:
代码语言:javascript复制1. 仅复制必要数据(非完全复制对象)
2. 防止未来被意外修改。
– 完结 –
相关链接
immer 官方文档