Immer 的使用以及在react中的应用
Immer是mobx的作者写的一个immutable库,核心实现是利用ES6的proxy,几乎以最小的成本实现了JS的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对JS不可变数据结构的需求。原文来源于https://www.cnblogs.com/ronffy/p/immer.html(记录一遍加深理解。)
数据存在的问题
引用类型对象存储地址,一旦被修改,原始的部分也被修改。
如下:
let currentState = {
p: {
x: [2],
},
a: [],
};
let copyData = currentState
copyData.p = 1 // currentState也被修改了
解决引用类型对象被修改的办法
1.深度拷贝,但是深拷贝的成本较高,会影响性能
2.ImmutableJS,非常棒的一个不可变数据结构的库,可以解决上面的问题,但是跟Immer比起来,ImmutableJS有两个不足:
需要使用者学习它的数据结构操作方式,没有Immer提供的使用原生对象的操作方式简单、易用
它的操作结果需要通过toJS方法才能得到原生对象,这使得在操作一个对象的时候,时刻要注意操作的是原生对象还是ImmutableJS的返回结果。
immer基本使用
语法:
produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState
-
currentState: 被操作对象的最初状态
-
draftState: 根据 currentState 生成的草稿状态,它是currentState的代理,对draftState 所做的任何修改都将被记录并用于生成 nextState。在此过程中,currentState 将不受影响
-
nextState:根据draftState 生成的最终状态
-
produce: 用来生成 nextState 或 producer 的函数
-
producer: 生产者 通过 produce 生成,用来生产nextState ,每次执行相同的操作
-
recipe: 生产机器 用来操作draftState 的函数
-
PatchListener可有可无的
常用api介绍
1.produce
//页面引入
import { produce} from "immer";
let currentState = {
p: {
x: [2],
},
a: [],
};
let nextState = produce(currentState, (draft) => {
// console.log(draft);// 依据currentState 封装的 Proxy对象
draft.a.push(1);
});
console.log(nextState === currentState); //false
console.log(nextState.p === currentState.p); // true
console.log(nextState.a === currentState.a); // false
由此可见,对 draftState 的修改都会反应到 nextState 上,而 Immer 使用的结构是共享的,nextState 在结构上又与 currentState 共享未修改的部分
自动冻结功能
Immer 还在内部做了一件很巧妙的事情,那就是通过 produce 生成的 nextState 是被冻结(freeze)的,(Immer 内部使用Object.freeze方法,只冻结 nextState 跟 currentState 相比修改的部分),这样,当直接修改 nextState 时,将会报错。
这使得 nextState 成为了真正的不可变数据。
//基于上面代码查看描述
console.log(Object.getOwnPropertyDescriptors(nextState)); //writable: false
console.log(Object.getOwnPropertyDescriptors(currentState)); //writable: true
对于冻结的对象仍然可以通过immer做处理,因为是基于原来对象做一层代理。
// nextState里面的属性是writeable:false
//这里说明可以对 属性为writeable:false的同样做处理
let nextState2 = produce(nextState,(draft)=>{
draft.a.push('fd')
})
console.log(nextState2 === nextState);
console.log(nextState.a === nextState2.a);
console.log(nextState.p=== nextState2.p);
console.log(Object.getOwnPropertyDescriptors(nextState)); //writable: false
console.log(Object.getOwnPropertyDescriptors(nextState2)); //writable: false
利用高阶函数的特点,提前生成一个生产者 producer,看起来更简洁
let producer = produce((draft)=>{
draft.x = 2
})
let nextState = producer(currentState)
2. recipe
返回值
recipe 没有返回值时:nextState 是根据 recipe 函数内的 draftState 生成的;
recipe 有返回值时:nextState 是根据 recipe 函数的返回值生成的;
let nextState = produce(currentState,(draft)=>{
return {
x:2
}
})
** recipe中的this**
recipe 函数内部的this指向 draftState ,也就是修改this与修改 recipe 的参数 draftState ,效果是一样的。
注意:此处的 recipe 函数不能是箭头函数,如果是箭头函数,this就无法指向 draftState 了
let nextState = produce(currentState,(draft)=>{
// 如果这里不是箭头函数 this 指向 draft 这里是箭头函数 就不是draft了
return {
x:2
}
})
3.patch补丁功能
通过此功能,可以方便进行详细的代码调试和跟踪,可以知道 recipe 内的做的每次修改,还可以实现时间旅行。
import {produce, applyPatches } from "immer"
let state={
x:1
}
let replaces = []
let inverseReplaces = []
state = produce(state,draft=>{
draft.x = 2
draft.y= 2
},(patches,inversePatches)=>{
// console.log(patches) // 存储了正向操作记录
console.log(inversePatches)// 存储了反向操作记录
replaces = patches.filter(patch=>patch.op ==='replace')
inverseReplaces =inversePatches.filter(patch=>patch.op ==='replace')
})
state = produce(state,draft=>{
draft.x = 3
})
// console.log(state)
state = applyPatches(state, replaces);
console.log('state2', state); // { x: 2, y: 2 }
state = produce(state,draft=>{
draft.x = 4
})
console.log(state)
state = applyPatches(state,inverseReplaces)
console.log(state)
// 以上实现了时间旅行
inversePatches数据结构如下:

用immer优化react项目的探索
首先定义一个state对象,后面的例子使用到变量state或访问this.state时,如无特殊声明,都是指这个state对象
state = {
members: [
{
name: 'limingyan',
age: 30
}
]
}
需求:members 成员中的第1个成员,年龄增加1岁
优化setState方法
错误示例
this.state.members[0].age++;
只所以有的新手同学会犯这样的错误,很大原因是这样操作实在是太方便了,以至于忘记了操作 State 的规则。(就是要通过setState改变值)
下面看下正确的实现方法
const { members } = this.state;
// 可以直接为对象 也可以接受函数
this.setState({
members: [
{
...members[0],
age: members[0].age + 1,
},
...members.slice(1),
]
})
this.setState(state => {
const { members } = state;
return {
members: [
{
...members[0],
age: members[0].age + 1,
},
...members.slice(1)
]
}
})
用immer更新state
// 一般写法
this.setState(state=>{
return produce(state,draft=>{
draft.members[0].age++;
})
})
// 高阶写法
this.setState(state=>{
return produce(draft=>{
draft.members[0].age++;
})(state)
})
// 简洁写法
this.setState(produce(draft => {
draft.members[0].age++;
}))
普通reducer怎样解决上面抛出的需求
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_AGE':
const { members } = state;
return {
...state,
members: [
{
...members[0],
age: members[0].age + 1,
},
...members.slice(1),
]
}
default:
return state
}
}
集合immer,reducer可以怎样写
const reducer = (state, action) => produce(state, draft => {
switch (action.type) {
case 'ADD_AGE':
draft.members[0].age++;
}
})
可以看到,通过 produce ,我们的代码量已经精简了很多;
不过仔细观察不难发现,利用 produce 能够先制造出 producer 的特点,代码还能更优雅:
const reducer = produce(draft => {
switch (action.type) {
case 'ADD_AGE':
draft.members[0].age++;
}
})(state, action)
// immer内部的produce方法 会把以上代码中 除了state后面的参数都给回掉函数,所以可以简写为以下
const reducer = produce((draft, action) => {
switch (action.type) {
case 'ADD_AGE':
draft.members[0].age++;
}
})