immer
可变状态:
1 | let objA = { name: "xiaoming" } |
我们只修改了 objB 的 name,发现 ojbA 也发生了改变。这个就是可变状态。
可变状态间接修改了其它对象,会造成代码隐患。
解决方案:
- 深度拷贝
- 使用 immer、immutable-js 等处理不可变数据的库
不可变数据 immutable
当我们使用 deepClone 或 immer / immutable-js 创建一个新对象,新对象进行有副作用(side effect)的操作都不会影响到原来的数据。这就是 immutable。
deepClone 虽然实现了 immutable,但是开销太大,因为它完全创建了一个新的对象出来,其实,对于不会进行赋值操作的 value 保持引用也没关系。
所以在 2014 年,facebook 的 immutable-js 横空出世,即保证了 immutable ,在运行时判断数据间的引用情况,又兼顾了性能。
immutable.js
immutable-js 使用了另一套数据结构的 API ,与我们的常见操作有些许不同,它将所有的原生数据类型(Object, Array 等)都会转化成 immutable-js 的内部对象(Map,List 等),并且任何操作最终都会返回一个新的 immutable 的值。
immer
Immer 是 mobx 的作者写的一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对 JS 不可变数据结构的需求。
与 immutable-js 最大的不同,immer 是使用原生数据结构的 API 而不是像 immutable-js 那样转化为内置对象之后使用内置的 API,举个简单例子:
1 | const produce = require("immer") |
通过上面的例子我们能发现,所有具有副作用的逻辑都可以放进 produce 的第二个参数的函数内部进行处理。在这个函数内部对原来的数据进行任何操作,都不会对原对象产生任何影响。
immer 原理
Immer 使用了 ES6 的新特性 Proxy 。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
immer 中的 proxy
immer 的做法就是维护一份 state 在内部,劫持所有操作,内部来判断是否有变化从而最终决定如何返回。下面这个例子就是一个构造函数,如果将它的实例传入 Proxy 对象作为第一个参数,后面处理对象时,就可以使用其中的方法:
1 | class Store { |
modified,source,copy 三个属性;get,set,modifing 三个方法。
modified 作为内置的 flag,判断如何进行设置和返回。
里面最关键的就应该是 modifing 这个函数,在第一次 set 的时候,实现一次 copy,copy 后的数据也是 immutable。
对于 Proxy 的第二个参数,简单做一层转发,任何对元素的读取和写入都转发到 store 实例内部方法去处理:
1 | const PROXY_FLAG = "@@SYMBOL_PROXY_FLAG" |
这里在 getter 里面加一个 flag 的目的就在于将来从 proxy 对象中获取 store 实例更加方便。
最终我们能够完成这个 produce 函数:
1 | function produce(state, producer) { |
这样,Store 构造函数、handler 处理对象,produce 处理 state,这三个模块最简版就完成了,将它们组合起来就是一个最 tiny 的 immer。真正的 immer 内部还有其他的功能。
当然,Proxy 作为一个新的 API,并不是所有环境都支持,Proxy 也无法 polyfill,所以 immer 在不支持 Proxy 的环境中,使用 Object.defineProperty 来进行一个兼容。
freeze
freeze 表示状态树在生成之后就被冻结不可继续操作。对于普通 JS 对象,我们可以使用 Object.freeze 来冻结我们生成的状态树对象,当然像 immer / immutable-js 内部自己有冻结的方法和逻辑。