React immer.js使用
React immer.js使用
基本介绍
一个基于react的第三方库,是一个很小的包,能以更方便的方式处理不可变状态(immutable state)。
解决的痛点 :让js对于复杂对象(嵌套较深)的修改变得更加容易。
原理: es6中proxy的概念。
原理
简化了不可变数据结构的处理
可用于需要使用不可变数据结构的任何上下文中。如react状态,react或react reducers或配置管理等。
- 针对不可变的数据结构能够做到变更检测
如果对象的引用没有更改,则对象本身也没有更改
如果没有在draft中对state对象做修改,那么返回值和原对象是一样的,绝对相等 - 使得克隆成本较低
原对象中,未更改的属性(树)部分不做复制,在内存中与原旧版本的属性共享属性 (树)。
通常来说,为了不更改原对象、数组或映射的任何属性,但又需要创建新对象并对其属性进行操作的时候 我们通常是对原对象进行深拷贝,然后通过操作拷贝的对象的属性来实现。
解决的痛点
- Immer会检测到意外变更并抛出错误。
- Immer能避免对不可变对象进行深度更新时,所需要的常规手动拷贝代码的实现。如果没有Immer,对象副本需要在每一级上手工创建其副本,通常通过使用很解构操作(…obj)操作。 当使用Immer时,只需要对 draft对象进行更改,draft对象会先记录用户的修改,然后仅创建有变更的必要的属性副本,不会影响原始对象。
- 在使用Immer时,您不需要额外学习专用的api或数据结构,使用普通的JavaScript数据结构并使用常规方式修改数据即可,操作简单且安全。
使用
我们可以利用 produce 函数,它的第一个参数为我们想要操作的初始的状态。 第二个参数是我们传递一个名为 recipe 的函数 该函数自动传入了一个 draft 对象作为参数,我们可以直接修改该 draft 对象。 一旦修改完成,这些修改将被记录下来并用于后续产生下一个状态。 之后,Produce 将负责将上面的变更进行必要的复制,并通对对象进行冻结,防止未来被意外修改。
import produce from 'immer';
const nextState = produce(baseState, (draft) => {
draft[1].done = true;
draft.push({ title: 'Tweet about it' });
});
优点
- 遵循不可变数据规范,同时使用普通的JavaScript对象、数组、集合和映射。不需要学习新的api或“语法”!
- 强类型,没有基于字符串的路径选择器等。 结构共享,仅复制需要的数据部分。
- 冻结对象,不会被轻易改变。
- 深度更新轻而易举,不需要人工考虑其数据结构会被影响或者遗漏。
- 使用简单,能使代码更简洁。
- 对JSON补丁的一流支持
- 体积小,gzipped 压缩后仅3 kb
应用场景
- 用于 React 的 state 的变更。
React 的 state 本身是不可修改的,当你需要修改它的某个属性然后保存为新的状态的时候,
使用 immer 可以很方便的获得一个新的 state。 - 需要复制一个不可变对象,在不改变原对象的情况下,修改其中的某个值,保存为一个新的对象。
- 复制一个不可变的数组,在不改变原数组的情况下,修改其中某个值,保存为新的数组。
类似深拷贝
import produce from 'immer';
const baseState = { a: 1 };
const nextState = produce(baseState, (draft) => {
draft[1].done = true;
draft.push({ title: 'Tweet about it' });
});
那为什么不用深拷贝
首先,深拷贝是完全复制,拷贝之后的对象和原对象有不同的堆内存存储空间
const baseState = [{ a: { b: 1 } }, { a: 2 }];
const nextState = JSON.parse(JSON.stringify(baseState));
console.log(nextState === baseState); // false
在react状态共享时,如果有其他组件用到了当前状态,用深拷贝会导致重新渲染,因为状态内容没变但内存地址发生了变化,用shouldComponentUpdate 判断的话,上一次的属性和新的属性是不一样的,所以要重新渲染。但是如果是 immer 的话就可以避免这种情况。
当然,即使是浅拷贝,新旧对象也不再相等,其对象的指针也会改变。 解构赋值是浅拷贝
const baseState = [{ a: { b: 1 } }, { a: 2 }];
const nextState = [...baseState];
console.log(nextState === baseState); // false
再来看immer
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的时候,如果没有变更,或者变更之后和原来一样就不会改变对象,其对象指针还是同一个。
如果对象有变更时,二者不再相等。
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为es6的proxy对象,draft 是个 Proxy 代理对象,对它的读写操作会走到内部定义的 getter/setter 里。
当访问 draft 时,其定义的 getter 会返回一个 Proxy 代理对象。
如果在 draft 中没有值的变更或者变更值和原对象一致,则返回原对象。
proxy
简单总结一下,就是 proxy 是源对象的代理,当你对代理进行读/写的时候,会进入到 handler 的 get, set 或者是 delete 等方法中,从而实现对对象的劫持。通过这种方式相当于获得了一个增强功能版的源对象,
Immer 仅适用于处理不可变对象
官网介绍:that allows you to work with immutable state in a more convenient way.
也就是说,immer 的根本目的是为了处理“不可变对象”而存在的(比如 React 的 state)。
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 的属性值
nextState.a.b = 999;
// 直接修改 baseState 的属性值
baseState.a.b = 999;
可以看到,报错了。 很显然,经过 immer 处理之后的 nextState 修改属性值的时候报错了。 而且,原对象 baseState 修改属性值的时候同样会报错。
- 仅复制必要数据(非完全复制对象)
- 防止未来被意外修改。
参考链接:
link.
link.