Immer.js 不可变数据,让你的操作更优雅~

前言

在官方文档里说的是: Immer 可让您以更方便的方式使用不可变状态。之前一直不明白这个库的意义,使用场景。感觉在项目中使用的场景并不多,后来接触了 React 之后,在写状态管理器的 reducer 时,就经常写出过类似的代码:

const reducer = (state, action) => {
  switch (action.type) {
    case 'type1':
      const { typeVal } = action.payload
      return {
        ...state,
        obj2: {
          ...state.obj2,
          type: typeVal,
        },
      }
  }
}

上述的代码并非不行,只是过多的 ... 便会稍微显得有些冗余,如果层数在更深,那更是头皮发麻。

使用 Immer 改写

那么我们想想,假设我们能够像下面这样书写,不就轻松很多了吗?

state.obj2.type = typeVal

但是我们 reducer 是一个纯函数,且不该改变 state 的状态,然后返回一个全新的 nextState,那么就不能够像上面的写法那样,因为上面的写法改变了源数据。

如何使得 state.obj2.type = typeVal 化为可能呢? Immer 就大显身手了~

查阅 zustand 文档时,看到了使用 Immer 来优化深层嵌套对象状态更新的例子。同理,当我们使用 Immer 来优化我们上面的 reducer 时,代码就会变成 ————

import produce from 'immer'

const reducer = (state, action) => {
  // 第一个参数放入 state
  // 第二个参数是一个函数,函数的第一个属性是 state的草稿状态,所以这里用 draft 来命名。
  // draft 和 state 拥有相同的属性,我们可以直接修改 draft,最终会返回一个全新的 newState
  return produce(state, (draft) => {
    switch (action.type) {
      case 'type1':
        const { typeVal } = action.payload
        draft.obj2.type = typeVal // 关键赋值
        break
    }
  })
}

我们发现,当使用 produce 包裹起来之后,我们就可以在第二个参数里使用 draft.obj2.type = typeVal。甚至不需要在 case 内部去 return draft; 因为 produce() 函数的返回值就是state

Immer 原理

那么这酷炫的操作是如何实现的呢?简单拜读了 Immer 的源码,分析下其原理。。

代码是从 produce(state, (draft)=> {}) 开始,那么我们就从这里出发,省略部分源码,我们会发现实际上,这里是创建了一个 proxy 对象。

// 这里的第二个参数,是 parent ,父对象,由于我们是第一层,所以并没有父对象,直接为 undefined
const proxy = createProxy(baseState, undefined)

然后我们会发现 createProxy 的实现,先定义了一个 state 对象。

const createProxy = (base, parent) => {
  // state 还有部分属性,就略过了,我们只重点看原理
  const state = {
    base, // 源对象,永远不变
    copy: null, // 当出发set访问器时,会修改该属性
    parent, // 父对象,用来回溯
    modified: false, // 判断是否被修改过(即是否发生过属性 set),该属性用来区分:我们后面使用 copy 还是 base
    proxies: {}, // 遇到普通的非proxy对象时,将其存储进该属性
  }

  // revocable 的作用是创建一个可撤销的 proxy 对象,使用 revoke 方法撤销,但因为我们这里只是简单介绍源码,就略过这一部分
  // objectTraps 是proxy的handle,源码里有针对多种情况,我这里只针对 对象 做考虑了。
  const { revoke, proxy } = Proxy.revocable(state, objectTraps)

  return proxy
}

好了,接下来就是通过 objectTraps 来处理访问器代码。后面详细代码里在说,先跳过这部分。接下来就是将 proxy 对象作为 draft参数 传入给用户提供的函数。

最后再将执行完的全新的 proxy ,在从中剥离出我们需要的对象即可。

简单的 Immer 就实现了,下面是全文代码。

// 源数据
const obj = {
name: 'pnm',
age: 10,
obj1: {
name: 'hello',
age: 11,
obj1A2: {
name: '66',
},
obj2: {
name: 'world',
age: 12,
obj2A2: {
name: '66',
},
},
},
}
// ============================================================================
const isProxy = (value) => {
// 如果是我们定义的 proxy 对象,会触发我们在get访问器中的约定 __PROXY_STATE__
return !!value && !!value['__PROXY_STATE__']
}
const isProxyable = (value) => {
if (!value) return false
if (typeof value !== 'object') return false
return true
}
const markChanged = (state) => {
if (!state.modified) {
state.modified = true
// 浅拷贝 base 并赋给 copy
state.copy = Object.assign({}, state.base)
console.log('copy <==== proxies', state.copy, state.proxies)
// 这一步很关键,因为 copy 里的是变化过的数据,proxies 中存放的是没有变化的proxy对象
Object.assign(state.copy, state.proxies)
// 子属性修改了,肯定父属性也就变了,往父辈回溯
if (state.parent) markChanged(state.parent)
}
}
const each = (value, cb) => {
for (let key in value) cb(key, value[key])
}
// ===========================================================================
const objectTraps = {
get(state, prop) {
// 【注】这里约定了:当get __PROXY_STATE__ 属性时,返回 state
if (prop == '__PROXY_STATE__') return state
if (isProxy(state[prop])) {
return state[prop]
}
if (state.modified) {
// 如果这个状态有被修改过了,那么处理 copy 里的值
const value = state.copy[prop]
// 当这个属性可以被 proxy化,且和源数据是同地址(同一个对象)
if (isProxyable(value) && value === state.base[prop]) {
return (state.copy[prop] = createProxy(value, state))
}
return value
} else {
// 没有被修改过则进入该条件中。
// 如果 proxies 中有这个属性,说明这个属性已经被proxy化,那么直接返回
if (state.proxies?.hasOwnProperty(prop)) return state.proxies[prop]
// 因为没被修改过,所以 copy 里没数据,则从 base 中获取数据。
const value = state.base[prop]
// 当这个值并非 proxy 且又为普通对象,那么创建proxy,并将其挂载到 proxies 的 [prop] 属性上
// 为了不影响源数据,所以 proxies 作为了 base 的替身。
if (!isProxy(value) && isProxyable(value)) {
return (state.proxies[prop] = createProxy(value, state))
}
return value
}
},
set(state, prop, value) {
// 该状态没有被修改过
if (!state.modified) {
// 如果 旧值 和 新值一样,说明没有改变,直接返回
if (
(prop in state.base && state.base[prop] === value) ||
(state.proxies?.hasOwnProperty(prop) && state.proxies[prop] === value)
) {
return true
}
// 改变 modified 为 true,并且浅拷贝 copy ,和 parent
markChanged(state)
}
// 这里将新值赋给copy,不改变源数据
state.copy[prop] = value
return true
},
}
const createProxy = (base, parent) => {
const state = {
base,
copy: null,
parent,
modified: false,
proxies: {},
}
const { proxy } = Proxy.revocable(state, objectTraps)
return proxy
}
const processResult = (proxy) => {
// 【注】这里的 __PROXY_STATE__ 并没有赋值过,只是一个约定,在 get 访问器中,约定了遇到  __PROXY_STATE__ 就返回 state
// 【注2】 为什么要如此做?
//  这是为了去除 proxy 带来的影响,因为如果直接 proxy.xx 属性,那么这个 xx 属性会进入到 get 访问器中。
//  如果先把 proxy 外壳去除,这样,我们的 state 就是一个 普通对象,那么调用 对象.属性,就不会进入到 proxy 的 get 访问器中
const state = proxy['__PROXY_STATE__']
let source = null
if (state.modified) {
source = state.copy
} else {
source = state.base
}
for (const key in source) {
if (Object.hasOwnProperty.call(source, key)) {
const tempState = source[key]
if (isProxy(tempState)) {
source[key] = processResult(tempState)
}
}
}
return source
}
// ============ 下方的produce 为暴露给用户使用的方法 ==================
const produce = (baseState, recipe) => {
/**
* ...省略部分源码
* 源码里做了一些判断,是否是函数,数组等,这里不过多介绍,只假设为 object 的情况
*/
const proxy = createProxy(baseState, undefined)
// 用户传入的方法,修改值并触发 proxy 的访问器
const result = recipe(proxy)
// 从 proxy 中剥离新的修改过的数据。
return processResult(proxy)
}
const newObj = produce(obj, (draft) => {
draft.obj1.obj2.age = 15
})
console.log({ newObj, obj })

这里有一个比较有意思的点,__PROXY_STATE__ 这个属性,我当时在源码里找了半天没找到这个属性在哪里赋值了,后来发现,这个属性不是赋值来的,而是一个约定,即:我 get 这个属性,那么你要给我一个非 proxy 的 state 对象

这里只是简单的将核心剖析了一下,详细源码还是建议大家阅读 Immer 的源码~这里就不做过多介绍了。

Immer.js 和 Immutable.js

简单提一嘴,还有一个和 Immer 同样的比较出名的库是 Immutable,但大部分情况下还是 Immer 会更好用些, Immutable 是 facebook 团队写的, Immer 是 mobx 作者写的。

Immutable 有自己的数据结构,使用的 API 比较多,上手难度会比 Immer 难一些,大部分情况下你使用 Immer,只需要记住 produce 即可,且库容量对比起来,Immer 更小巧轻量,gzip 压缩一下才 3kb,所以建议如果有使用不可变数据的情况下, Immer 会是你不错的选择

原文链接:https://juejin.cn/post/7335661961902227482 作者:pnm学编程

(0)
上一篇 2024年2月17日 上午10:27
下一篇 2024年2月17日 上午10:37

相关推荐

发表回复

登录后才能评论