摘要:无奈网络上完善的文档实在太少,所以自己写了一份,本篇文章以贴近实战的思路和流程,对进行了全面的讲解。这使得成为了真正的不可变数据。的使用非常灵活,多多思考,相信你还可以发现更多其他的妙用参考文档官方文档
文章在 github 开源, 欢迎 Fork 、Star前言
Immer 是 mobx 的作者写的一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对JS不可变数据结构的需求。
无奈网络上完善的文档实在太少,所以自己写了一份,本篇文章以贴近实战的思路和流程,对 Immer 进行了全面的讲解。
先定义一个初始对象,供后面例子使用:
首先定义一个currentState对象,后面的例子使用到变量currentState时,如无特殊声明,都是指这个currentState对象
let currentState = { p: { x: [2], }, }
哪些情况会一不小心修改原始对象?
// 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 被修改了解决引用类型对象被修改的办法
深度拷贝,但是深拷贝的成本较高,会影响性能;
ImmutableJS,非常棒的一个不可变数据结构的库,可以解决上面的问题,But,跟 Immer 比起来,ImmutableJS 有两个较大的不足:
需要使用者学习它的数据结构操作方式,没有 Immer 提供的使用原生对象的操作方式简单、易用;
它的操作结果需要通过toJS方法才能得到原生对象,这使得在操作一个对象的时候,时刻要注意操作的是原生对象还是 ImmutableJS 的返回结果,稍不注意,就会产生意想不到的 bug。
看来目前已知的解决方案,我们都不甚满意,那么 Immer 又有什么高明之处呢?
immer功能介绍 安装immer欲善其事必先利其器,安装 Immer 是当前第一要务
npm i --save immerimmer如何fix掉那些不爽的问题
Fix Q1、Q3
import produce from "immer"; let o1 = produce(currentState, draft => { draft.p.x = 1; })
Fix Q2
import produce from "immer"; fn(currentState); function fn(o) { return produce(o, draft => { draft.p1 = 1; }) };
Fix Q4
import produce from "immer"; let o4 = produce(currentState, draft => { draft.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 是完全相同的
produce备注:出现PatchListener先行跳过,后面章节会做介绍
语法:
produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState
例子1:
let nextState = produce(currentState, (draft) => { }) currentState === nextState; // true
例子2:
let currentState = { a: [], p: { x: 1 } } let nextState = produce(currentState, (draft) => { draft.a.push(2); }) 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 成为了真正的不可变数据。
例子:
let nextState = produce(currentState, (draft) => { draft.p.x.push(2); }) currentState === nextState; // true
利用高阶函数的特点,提前生成一个生产者 producer
语法:
produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState
例子:
let producer = produce((draft) => { draft.x = 2 }); let nextState = producer(currentState);
recipe 是否有返回值,nextState 的生成过程是不同的:
recipe 没有返回值时:nextState 是根据 recipe 函数内的 draftState 生成的;
recipe 有返回值时:nextState 是根据 recipe 函数的返回值生成的;
let nextState = produce( currentState, (draftState) => { return { x: 2 } } )
此时,nextState 不再是通过 draftState 生成的了,而是通过 recipe 的返回值生成的。
recipe 函数内部的this指向 draftState ,也就是修改this与修改 recipe 的参数 draftState ,效果是一样的。
注意:此处的 recipe 函数不能是箭头函数,如果是箭头函数,this就无法指向 draftState 了
produce(currentState, function(draft){ // 此处,this 指向 draftState draft === this; // true })patch补丁功能
通过此功能,可以方便进行详细的代码调试和跟踪,可以知道 recipe 内的做的每次修改,还可以实现时间旅行。
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, draft => { draft.x = 2; draft.y = 2; }, (patches, inversePatches) => { replaces = patches.filter(patch => patch.op === "replace"); inverseReplaces = inversePatches.filter(patch => patch.op === "replace"); } ) state = produce(state, draft => { draft.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, draft => { draft.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次打印结果分别是:3、2、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 项目的开发效率。
用immer优化react项目的探索首先定义一个state对象,后面的例子使用到变量state或访问this.state时,如无特殊声明,都是指这个state对象
state = { members: [ { name: "ronffy", age: 30 } ] }抛出需求
就上面定义的state,我们先抛一个需求出来,好让后面的讲解有的放矢:
members 成员中的第1个成员,年龄增加1岁
this.state.members[0].age++;
只所以有的新手同学会犯这样的错误,很大原因是这样操作实在是太方便了,以至于忘记了操作 State 的规则。
下面看下正确的实现方法
setState的第1种实现方法const { members } = this.state; this.setState({ members: [ { ...members[0], age: members[0].age + 1, }, ...members.slice(1), ] })setState的第2种实现方法
this.setState(state => { const { members } = state; return { members: [ { ...members[0], age: members[0].age + 1, }, ...members.slice(1) ] } })
以上2种实现方式,就是setState的两种使用方法,相比大家都不陌生了,所以就不过多说明了,接下来看下,如果用 Immer 解决,会有怎样的烟火?
用immer更新statethis.setState(produce(draft => { draft.members[0].age++; }))
是不是瞬间代码量就少了很多,阅读起来舒服了很多,而且更易于阅读了。
优化reducer immer的produce的拓展用法在开始正式探索之前,我们先来看下 produce 第2种使用方式的拓展用法:
例子:
let obj = {}; let producer = produce((draft, 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 } }集合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, action) => { switch (action.type) { case "ADD_AGE": draft.members[0].age++; } })
好了,至此,Immer 优化 reducer 的方法也讲解完毕。
Immer 的使用非常灵活,多多思考,相信你还可以发现 Immer 更多其他的妙用!
参考文档官方文档
Introducing Immer: Immutability the easy way
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/99761.html
摘要:例如维护一份在内部,来判断是否有变化,下面这个例子就是一个构造函数,如果将它的实例传入对象作为第一个参数,就能够后面的处理对象中使用其中的方法上面这个构造函数相比源代码省略了很多判断的部分。 showImg(https://segmentfault.com/img/bV27Dy?w=1400&h=544); 博客链接:下一代状态管理工具 immer 简介及源码解析 JS 里面的变量类...
摘要:所以整个过程只涉及三个输入状态,中间状态,输出状态关键是是如何生成,如何应用修改,如何生成最终的。至此基本把上的模式解析完毕。结束实现还是相当巧妙的,以后可以在状态管理上使用一下。 开始 在函数式编程中,Immutable这个特性是相当重要的,但是在Javascript中很明显是没办法从语言层面提供支持,但是还有其他库(例如:Immutable.js)可以提供给开发者用上这样的特性,所...
摘要:所以整个过程只涉及三个输入状态,中间状态,输出状态关键是是如何生成,如何应用修改,如何生成最终的。至此基本把上的模式解析完毕。结束实现还是相当巧妙的,以后可以在状态管理上使用一下。 开始 在函数式编程中,Immutable这个特性是相当重要的,但是在Javascript中很明显是没办法从语言层面提供支持,但是还有其他库(例如:Immutable.js)可以提供给开发者用上这样的特性,所...
阅读 1444·2023-04-26 00:25
阅读 874·2021-09-27 13:36
阅读 899·2019-08-30 14:14
阅读 2140·2019-08-29 17:10
阅读 982·2019-08-29 15:09
阅读 1924·2019-08-28 18:21
阅读 935·2019-08-26 13:27
阅读 944·2019-08-26 10:58