immer

本文最后更新于:2023年12月5日 晚上

可变状态:

let objA = { name: "xiaoming" }
let objB = objA
objB.name = "lihua"
console.log(objA.name) // lihua

我们只修改了 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,举个简单例子:

const produce = require("immer")

const state = {
 done: false,
 val: "string",
}

// 所有具有副作用的操作,都可以放入 produce 函数的第二个参数内进行
// 最终返回的结果并不影响原来的数据
const newState = produce(state, draft => {
 draft.done = true
})

console.log(state.done) // false
console.log(newState.done) // true

通过上面的例子我们能发现,所有具有副作用的逻辑都可以放进 produce 的第二个参数的函数内部进行处理。在这个函数内部对原来的数据进行任何操作,都不会对原对象产生任何影响。

immer 原理

Immer 使用了 ES6 的新特性 Proxy 。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

immer 中的 proxy

immer 的做法就是维护一份 state 在内部,劫持所有操作,内部来判断是否有变化从而最终决定如何返回。下面这个例子就是一个构造函数,如果将它的实例传入 Proxy 对象作为第一个参数,后面处理对象时,就可以使用其中的方法:

class Store {
 constructor(state) {
  this.modified = false
  this.source = state
  this.copy = null
 }
 get(key) {
  if (!this.modified) return this.source[key]
  return this.copy[key]
 }
 set(key, value) {
  if (!this.modified) this.modifing()
  return (this.copy[key] = value)
 }
 modifing() {
  if (this.modified) return
  this.modified = true
  // 这里使用原生的 API 实现一层 immutable,
  // 数组使用 slice 则会创建一个新数组。对象则使用解构
  this.copy = Array.isArray(this.source) ? this.source.slice() : { ...this.source }
 }
}

modified,source,copy 三个属性;get,set,modifing 三个方法。

modified 作为内置的 flag,判断如何进行设置和返回。

里面最关键的就应该是 modifing 这个函数,在第一次 set 的时候,实现一次 copy,copy 后的数据也是 immutable。

对于 Proxy 的第二个参数,简单做一层转发,任何对元素的读取和写入都转发到 store 实例内部方法去处理:

const PROXY_FLAG = "@@SYMBOL_PROXY_FLAG"
const handler = {
 get(target, key) {
  // 如果遇到了这个 flag 我们直接返回我们操作的 target
  if (key === PROXY_FLAG) return target
  return target.get(key)
 },
 set(target, key, value) {
  return target.set(key, value)
 },
}

这里在 getter 里面加一个 flag 的目的就在于将来从 proxy 对象中获取 store 实例更加方便。

最终我们能够完成这个 produce 函数:

function produce(state, producer) {
 const store = new Store(state)
 const proxy = new Proxy(store, handler)

 // 执行我们传入的 producer 函数,我们实际操作的都是 proxy 实例,所有有副作用的操作都会在 proxy 内部进行判断,是否最终要对 store 进行改动。
 producer(proxy)

 // 处理完成之后,通过 flag 拿到 store 实例
 const newState = proxy[PROXY_FLAG]
 if (newState.modified) return newState.copy
 return newState.source
}

这样,Store 构造函数、handler 处理对象,produce 处理 state,这三个模块最简版就完成了,将它们组合起来就是一个最 tiny 的 immer。真正的 immer 内部还有其他的功能。

当然,Proxy 作为一个新的 API,并不是所有环境都支持,Proxy 也无法 polyfill,所以 immer 在不支持 Proxy 的环境中,使用 Object.defineProperty 来进行一个兼容。

freeze

freeze 表示状态树在生成之后就被冻结不可继续操作。对于普通 JS 对象,我们可以使用 Object.freeze 来冻结我们生成的状态树对象,当然像 immer / immutable-js 内部自己有冻结的方法和逻辑。


immer
http://blog.lujinkai.cn/前端/React/immer/
作者
像方便面一样的男子
发布于
2023年5月29日
更新于
2023年12月5日
许可协议