React 作为当前前端最受欢迎的框架之一,极大的提升了前端开发效率。
其拥有庞大的开发者群体,其社区也非常活跃,因此围绕 React 也产出了非常多的第三方库。
Immer 就是其中之一。
一、Immer基本介绍
1 | Immer (German for: always) is a tiny package that allows you to work with |
Immer 是一个很小的包,它能让您以更方便的方式处理不可变状态(immutable state)。
是 2019 年 React“年度突破”开源奖和“最具影响力贡献”JavaScript 开源奖得主。
Immer 简化了不可变数据结构的处理
Immer 可用于需要使用不可变数据结构的任何上下文中。
例如,结合 React 状态、React 或 Redux reducers 或配置管理等。
针对不可变的数据结构能够做到变更检测:
1 | 即如果对象的引用没有更改,则对象本身也没有更改。 |
此外,它还使得克隆成本相对较低:
1 | 原对象中,未更改的属性(树)部分不做复制,在内存中与原旧版本的属性共享属性(树)。 |
通常来说,为了不更改原对象、数组或映射的任何属性,但又需要创建新对象并对其属性进行操作的时候
我们通常是对原对象进行深拷贝,然后通过操作拷贝的对象的属性来实现。
但是,这在实践中,可能导致写出相当繁琐的代码,并且很容易意外影响到原对象。
Immer 的出现就是为了解决这些问题,它能解决如下痛点:
1 | 1.Immer会检测到意外变更并抛出错误。 |
为什么要使用Immer?
假如有如下 state 数组:
1 | const baseState = [ |
我们需要将 baseState 数组的状态进行变更,变更为一个新的 state 状态
同时,原本的 baseState 不能被修改。
1 | 1)对第二项数据的 done 值进行变更为 true |
如果不使用 Immer
我们将不得不小心翼翼地浅层复制状态结构的每一层,这将取决于我们的手工操作是否仔细。
1 | const nextState = baseState.slice(); // 浅复制数组对象 |
使用 Immer
使用 Immer,能让这个过程更直接。
我们可以利用 produce 函数,它的第一个参数为我们想要操作的初始的状态。
第二个参数是我们传递一个名为 recipe 的函数
该函数自动传入了一个 draft 对象作为参数,我们可以直接修改该 draft 对象。
一旦修改完成,这些修改将被记录下来并用于后续产生下一个状态。
之后,Produce 将负责将上面的变更进行必要的复制,并通对对象进行冻结,防止未来被意外修改。
实现代码如下:
1 | import produce from 'immer'; |
很显然,使用 immer 之后,相比于之前手工实现简单了不少,失误的可能性更低了。
同时, produce 对其对象的冻结也避免了其在此后的操作中被意外修改的可能性。
1 | Immer 就像是一个私人助理。 |
Immer 的优势
1 | 1. 遵循不可变数据规范,同时使用普通的JavaScript对象、数组、集合和映射。不需要学习新的api或“语法”! |
二、Immer的使用场景
应用的场景有:
1 | 1. 用于 React 的 state 的变更。 |
类似于深拷贝:
1 | import produce from 'immer'; |
如上代码,baseState 为原状态,draft 可以看做是 baseState 的深拷贝对象(其实不是,它是一个代理对象)。
当然,其效果和深拷贝对象是非常类似的,和操作一个对象的完全复制体一样,修改 draft 的时候并不会影响原来的 baseState。
为什么不直接使用深拷贝呢?
上面说了,draft 既然可以看做是 baseState 的深拷贝对象,为什么不直接使用深拷贝呢?
还是有区别的,因为 immer 处理对象也仅仅是看起来像是深拷贝,其实不是,还是有一些区别的。
首先,深拷贝是完全复制,拷贝之后的对象和原对象有不同的堆内存存储空间。
1 | const baseState = [{ a: { b: 1 } }, { a: 2 }]; |
深拷贝之后其对象的指针已经不一样了,因此拷贝之后对象和原对象并不相等。
当然,即使是浅拷贝,新旧对象也不再相等,其对象的指针也会改变。
如下代码所示:
1 | const baseState = [{ a: { b: 1 } }, { a: 2 }]; |
再来看 immer:
1 | import produce from 'immer'; |
可以看出,经过 immer 处理之后,两个对象竟然是相等的。
这是因为,immer 在处理 draft的时候,如果没有变更,或者变更之后和原来一样就不会改变对象,其对象指针还是同一个。
那么如果 draft 内部处理的时候有变更呢?
如下代码所示:
1 | import produce from 'immer'; |
从打印结果就可以看出来,当 draft 修改对象属性之后,二者就不再相等了。
显然 produce 返回的 nextState 对象和原来的 baseState不一样了。
这是为什么呢?
原因就在于 Immer draft 的实现:
1 | draft 是个 Proxy 代理对象,对它的读写操作会走到内部定义的 getter/setter 里。 |
Immer 仅适用于处理不可变对象
我们可以再回头看看前文中 immer 基本介绍的那一句英文,此为官网的原文。
1 | that allows you to work with immutable state in a more convenient way. |
with immutable state
不可变对象。
也就是说,immer 的根本目的是为了处理“不可变对象”而存在的(比如 React 的 state)。
为什么说是为了处理不可变对象呢?
对普通对象难道不行吗?
1 | 最好不要。 |
当然,也可以试试看,比如如下代码:
1 | import produce from 'immer'; |
此时,我们来修改一下 nextState/baseState 对象的属性值:
1 | // 直接修改 nextState 的属性值 |
1 | // 直接修改 baseState 的属性值 |
可以看到,报错了。
很显然,经过 immer 处理之后的 nextState 修改属性值的时候报错了。
而且,原对象 baseState 修改属性值的时候同样会报错。
1 | immer 改变了原对象!!! |
原因也不难理解,毕竟这就是Immer的两大“亮点”:
1 | 1. 仅复制必要数据(非完全复制对象) |
– 完结 –