文章在 github 开源, 欢迎 Fork 、Star !javascript
Immer 是 mobx 的做者写的一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,知足了咱们对JS不可变数据结构的需求。
无奈网络上完善的文档实在太少,因此本身写了一份,本篇文章以贴近实战的思路和流程,对 Immer 进行了全面的讲解。java
先定义一个初始对象,供后面例子使用: 首先定义一个currentState
对象,后面的例子使用到变量currentState
时,如无特殊声明,都是指这个currentState
对象react
let currentState = {
p: {
x: [2],

},
}
复制代码
哪些状况会一不当心修改原始对象?git
// Q1
let o1 = currentState;
o1.p = 1; // currentState 被修改了
o1.p.x = 1; // currentState 被修改了
// Q2
fn(currentState); // currentState 被修改了
function fn(o) {
o.p1 = 1;
return o;
};
// Q3
let o3 = {
...currentState
};
o3.p.x = 1; // currentState 被修改了
// Q4
let o4 = currentState;
o4.p.x.push(1); // currentState 被修改了
复制代码
toJS
方法才能获得原生对象,这使得在操做一个对象的时候,时刻要注意操做的是原生对象仍是 ImmutableJS 的返回结果,稍不注意,就会产生意想不到的 bug。看来目前已知的解决方案,咱们都不甚满意,那么 Immer 又有什么高明之处呢?github
欲善其事必先利其器,安装 Immer 是当前第一要务typescript
npm i --save immer
复制代码
Fix Q一、Q3shell
import produce from 'immer';
let o1 = produce(currentState, draftState => {
draftState.p.x = 1;
})
复制代码
Fix Q2npm
import produce from 'immer';
fn(currentState);
function fn(o) {
return produce(o, draftState => {
draftState.p1 = 1;
})
};
复制代码
Fix Q4api
import produce from 'immer';
let o4 = produce(currentState, draftState => {
draftState.p.x.push(1);
})
复制代码
是否是使用很是简单,经过小试牛刀,咱们简单的了解了 Immer ,下面将对 Immer 的经常使用 api 分别进行介绍。数组
Immer 涉及概念很少,在此将涉及到的概念先行罗列出来,阅读本文章过程当中遇到不明白的概念,能够随时来此处查阅。
currentState
被操做对象的最初状态
draftState
根据 currentState 生成的草稿状态,它是 currentState 的代理,对 draftState 所作的任何修改都将被记录并用于生成 nextState 。在此过程当中,currentState 将不受影响
nextState
根据 draftState 生成的最终状态
produce 生产
用来生成 nextState 或 producer 的函数
producer 生产者
经过 produce 生成,用来生产 nextState ,每次执行相同的操做
recipe 生产机器
用来操做 draftState 的函数
使用 Immer 前,请确认将immer
包引入到模块中
import produce from 'immer'
复制代码
or
import { produce } from 'immer'
复制代码
这两种引用方式,produce 是彻底相同的
备注:出现PatchListener
先行跳过,后面章节会作介绍
语法:
produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState
例子1:
let nextState = produce(currentState, (draftState) => {
})
currentState === nextState; // true
复制代码
例子2:
let currentState = {
a: [],
p: {
x: 1
}
}
let nextState = produce(currentState, (draftState) => {
draftState.a.push(2);
})
currentState === nextState // false
currentState.a === nextState.a; // false
currentState.p === nextState.p; // true
复制代码
因而可知,对 draftState 的修改都会反映到 nextState 上。而 Immer 使用的结构是共享的,nextState 在结构上与 currentState 共享未修改的部分,共享效果如图(借用的一篇 Immutable 文章中的动图,侵删):
Immer 还在内部作了一件很巧妙的事情,那就是经过 produce 生成的 nextState 是被冻结(freeze)的,(Immer 内部使用Object.freeze
方法,只冻结 nextState 跟 currentState 相比修改的部分),这样,当直接修改 nextState 时,将会报错。 这使得 nextState 成为了真正的不可变数据。
示例:
const currentState = {
p: {
x: [2],
},
};
const nextState = produce(currentState, draftState => {
draftState.p.x.push(3);
});
console.log(nextState.p.x); // [2, 3]
nextState.p.x = 4;
console.log(nextState.p.x); // [2, 3]
nextState.p.x.push(5); // 报错
复制代码
利用高阶函数的特色,生成一个生产者 producer
语法:
produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState
例子:
let producer = produce((draftState) => {
draftState.x = 2
});
let nextState = producer(currentState);
复制代码
recipe 是否有返回值,nextState 的生成过程不一样:
recipe 没有返回值时:nextState 根据 draftState 生成;
recipe 有返回值时:nextState 根据 recipe 函数的返回值生成;
let nextState = produce(currentState, (draftState) => {
return {
x: 5
}
}
)
console.log(nextState); // {x: 5}
复制代码
此时,nextState 再也不是经过 draftState 生成,而是经过 recipe 的返回值生成。
recipe 函数内部的this
指向 draftState ,也就是修改this
与修改 recipe 的参数 draftState ,效果是同样的。
注意:此处的 recipe 函数不能是箭头函数,若是是箭头函数,this
就没法指向 draftState 了
produce(currentState, function(draftState){
// 此处,this 指向 draftState
draftState === this; // true
})
复制代码
经过此功能,能够方便进行详细的代码调试和跟踪,能够知道 draftState 的每次修改,还能够实现时间旅行。
Immer 中,一个 patch 对象以下:
interface Patch {
op: "replace" | "remove" | "add" // 一次更改的动做类型
path: (string | number)[] // 此属性指从树根到被更改树杈的路径
value?: any // op为 replace、add 时,才有此属性,表示新的赋值
}
复制代码
语法:
produce(
currentState,
recipe,
// 经过 patchListener 函数,暴露正向和反向的补丁数组
patchListener: (patches: Patch[], inversePatches: Patch[]) => void
)
applyPatches(currentState, changes: (patches | inversePatches)[]): nextState
复制代码
例子:
import produce, { applyPatches } from "immer"
let state = {
x: 1
}
let replaces = [];
let inverseReplaces = [];
state = produce(
state,
draftState => {
draftState.x = 2;
draftState.y = 2;
},
(patches, inversePatches) => {
replaces = patches.filter(patch => patch.op === 'replace');
inverseReplaces = inversePatches.filter(patch => patch.op === 'replace');
}
)
state = produce(state, draftState => {
draftState.x = 3;
})
console.log('state1', state); // { x: 3, y: 2 }
state = applyPatches(state, replaces);
console.log('state2', state); // { x: 2, y: 2 }
state = produce(state, draftState => {
draftState.x = 4;
})
console.log('state3', state); // { x: 4, y: 2 }
state = applyPatches(state, inverseReplaces);
console.log('state4', state); // { x: 1, y: 2 }
复制代码
state.x
的值4次打印结果分别是:三、二、四、1
,实现了时间旅行, 能够分别打印patches
和inversePatches
看下,
patches
数据以下:
[
{
op: "replace",
path: ["x"],
value: 2
},
{
op: "add",
path: ["y"],
value: 2
},
]
复制代码
inversePatches
数据以下:
[
{
op: "replace",
path: ["x"],
value: 1
},
{
op: "remove",
path: ["y"],
},
]
复制代码
可见,patchListener
内部对数据操做作了记录,并分别存储为正向操做记录和反向操做记录,供咱们使用。
至此,Immer 的经常使用功能和 api 咱们就介绍完了。
接下来,咱们看如何用 Immer ,提升 React 、Redux 项目的开发效率。
首先定义一个state
对象,后面的例子使用到变量state
或访问this.state
时,如无特殊声明,都是指这个state
对象
state = {
members: [
{
name: 'ronffy',
age: 30
}
]
}
复制代码
就上面定义的state
,咱们先抛一个需求出来,好让后面的讲解有的放矢:
members 成员中的第1个成员,年龄增长1岁
this.state.members[0].age++;
复制代码
只因此有的新手同窗会犯这样的错误,很大缘由是这样操做实在是太方便了,以致于忘记了操做 state 的规则。
下面看下正确的实现方法
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)
]
}
})
复制代码
以上2种实现方式,就是setState
的两种使用方法,想必你们都不陌生。接下来看下,若是用 Immer 解决,会有怎样的烟火?
this.setState(produce(draftState => {
draftState.members[0].age++;
}))
复制代码
是否是马上代码量就少了不少,并且更易于阅读。
在开始正式探索以前,咱们先来看下 produce 第2种使用方式的拓展用法:
例子:
let obj = {};
let producer = produce((draftState, arg) => {
obj === arg; // true
});
let nextState = producer(currentState, obj);
复制代码
相比 produce 第2种使用方式的例子,多定义了一个obj
对象,并将其做为 producer 方法的第2个参数传了进去;能够看到, produce 内的 recipe 回调函数的第2个参数与obj
对象是指向同一块内存。
ok,咱们在知道了 produce 的这种拓展用法后,看看可以在 Redux 中发挥什么功效?
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
}
}
复制代码
const reducer = (state, action) => produce(state, draftState => {
switch (action.type) {
case 'ADD_AGE':
draftState.members[0].age++;
}
})
复制代码
能够看到,经过 produce ,咱们的代码量已经精简了不少;
不过,仔细观察不难发现,利用 produce 可以制造 producer 的特色,代码还能更优雅:
const reducer = produce((draftState, action) => {
switch (action.type) {
case 'ADD_AGE':
draftState.members[0].age++;
}
})
复制代码
好了,至此,Immer 优化 reducer 的方法也讲解完毕。
Immer 的使用很是灵活,并且还有一些其余拓展 api ,多多研究,相信你还能够发现 Immer 更多其余的妙用!