Immer数据不可变魔法

62 阅读3分钟

为什么是数据不可变?

  • JS中数据是可变的,因为是引用型数据,指向同一块地址,于是存在一个问题:改变新的对象会影响到原对象的值;当应用复杂之后,往往会有预想不到的“效果”
  • 数据不可变的核心是:创建原数据的副本,返回新数据,保持原数据不变
  • 常见的解决方法是深浅拷贝,但是这种做法除了需要开辟新的地址空间,耗费性能和内存;还存在另外一个问题就是,开发者需要层层解构、拷贝十分繁琐
  • 于是immer应运而生
    • immer的本质是针对传入的数据创建一个Proxy代理对象(称为Draft草稿对象),在其内部通过记录草稿对象的所有变更
    • 在最终返回的时候,通过变更记录反向修改,尽可能复用未修改的部分,也就是结构共享;这里还有一个好处是:未修改的部分地址不变,在应用中引用到未修改部分的状态的组件就可以避免不必要的重新渲染

    这一点可见"代码实现" -> "说明"部分

    • 上述便是immer的核心:Proxy、Draft、结构共享

case:

文章将以zhstandstore.state管理为例,简单分享下immer的工作机制 & 为啥它好

import { create } from 'zustand'
import { produce } from 'immer'
import type { userStoreType } from '@/types/user'

const useUserStore = create<userStoreType>((set) => {
  return {
    age: ,
    info: {
      token: ,
      email: ,
      deep: {
          userName: ,
      },
      id: ,
    },
    changeUserName: (userName) => {
        return set((state) => {
            return {
                info: {
                    ...state.info,
                    deep: {
                        ...state.info.deep,
                        userName,
                    }
                }
            }
        })
    },
    immerChangeName: (val) => {
        return set(
            produce((state) => {
                state.info.deep.userName = val
            })
        )
    }
  }
})

export default useUserStore

当我想修改userName字段的话

  • changeUserName需要层层解构
  • immerChangeName可以以一种直接访问的方式修改目标数据

代码实现:

const state = {
  user: {
    name: 'zs',
    age: 10,
    address: {
      city: 'gd',
      street: 'xxx',
    },
  },
}

function isPlainObject(value) {
  if (typeof value !== 'object' || value === null) {
    return false
  }
  let proto = Object.getPrototypeOf(value)
  if (proto === null || proto === Object.prototype) {
    return true
  }
  return false
}

function produce(state, recipe) {
  // 缓存被修改的对象
  let copies = new Map()

  const handler = {
    get(target, prop) {
      // 动态递归监听,此处可联想到 Vue3 响应式系统的处理方法,思想一致
      const value = target[prop]
      return isPlainObject(value) ? new Proxy(value, handler) : value
    },
    set(target, prop, value) {
      const cur = copies.has(target) ? copies.get(target) : target
      const copy = {
        ...cur,
        [prop]: value,
      }
      copies.set(target, copy)
    },
  }

  // 检查对象是否发送变化
  function hasChanges(base) {
    if (!isPlainObject(base)) {
      return false
    }
    if (copies.has(base)) {
      return true
    }
    
    const keys = Object.keys(base)
    for (let i = 0; i < keys.length; i++) {
      if (hasChanges(base[keys[i]])) {
        return true
      }
    }
    return false
  }

  // 反向操作,对应字段如果被修改过,则修改回去,否则返回原内容
  function finalize(state) {
    if (!hasChanges(state)) {
      return state
    }

    const result = copies.has(state) ? copies.get(state) : { ...state }
    Object.keys(state).map((key) => {
      result[key] = finalize(result[key])
    })
    return result
  }

  const draft = new Proxy(state, handler)
  recipe(draft)
  return finalize(state)
}

const res = produce(state, (draft) => {
  draft.user.name = 'zs'
  // draft.user.age = 20
  // draft.user.address.street = '10000'
})

/**
 * 1. copies 记录每一个修改操作,避免全量深拷贝
 * 2. finalize 递归构建新状态,确保只有修改的部分更新,而为修改的部分保持原引用,从而达到数据不可变的效果
 */
console.log(res, state)
console.log(res.user.name)
console.log(res.user === state.user)
console.log(res.user.address === state.user.address)

说明:

假设我们当前修改的是user.name,执行并查看打印结果: image.png 可以看到res.user.address === state.user.address结果为true,也就意味着此次修改中address没有被一股脑、粗暴地修改,那么当应用中存在代码:

const city = useUserStore((state) => state.user.address.city)
// ...
<div>{city}</div>

的时候,并不会因为此次与自己无关的修改而被动的重新渲染。所以,immer可不仅仅是方便了开发者修改数据,搭配react更新渲染机制,食用效果更佳!