不可变数据实现-Immer.js

不可变数据实现-Immer.js

一、 Immer.js是什么?

Immer.js mobx 的作者写的一个 Immutable (不可变数据) 库,同时 Immer 在2019年获得 JavaScript Open Source Award 大奖。核心实现是利用 ES6 的 proxy ,几乎以最小的成本实现了 JavaScript 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对 JS 不可变数据结构的需求。

二、 什么是不可变数据?

首先需要先理解什么是可变数据:举个例子

let objA = { name: '小明' };
let objB = objA;
objB.name = '小红';
console.log(objA.name); // objA 的name也变成了小红

像这样我们明明只修改代码 objB name ,发现 objA 也发生了改变,这个就是可变数据。

那不可变数据是什么怩?

不可变数据概念来源于函数式编程。函数式编程中,对已初始化的“变量”是不可以更改的,每次更改都要创建一个新的“变量”。新的数据进行有副作用的操作都不会影响之前的数据。这也就是 Immutable 的本质。

JavaScript 在语言层没有实现不可变数据,需要借助第三方库来实现。 Immer.js 就是其中一种实现(类似的还有 Immutable.js )。

三、 为什么要追求不可变数据?

  • 数据拷贝处理中存在的问题
var testA = [{value:1}]
var testB = testA.map(item => item.value =2)
//问题:本意只是让testB的每个元素变为2、却无意改掉了testA每个元素的结果
//解决:当需要传递一个引用类型的变量进一个函数时,可以使用Object.assign或者...解构,断引用
var testB = testA.map(item =>({...item, item.value =2}))
//问题:Object.assign或者...只会断开一层引用,但如果对象嵌套超过一层
// 深层次的对象嵌套
var testA = [{
    value: 1,
    desc: { text: 'a' }
var testB = testA.map(item => ({ ...item, value: 2 }))
console.log(testA === testB)           // false
console.log(testA.desc === testB.desc) // true
// testA.desc和testB.desc指向同一个引用
//解决:深拷贝,递归去遍历
//只考虑对象的场景
function deepClone(obj) {
  const keys = Object.keys(obj)
  return keys.reduce((memo, current) => {
    const value = obj[current]
    if (typeof value === 'object') {
      return {
        ...memo,
        [current]: deepClone(value),
    return {
      ...memo,
      [current]: value,
  }, {})
// deepClone可以满足简单的需求,但在实际开发中,需要考虑其他因素:如原型链上的处理,value出现循环引用的场景、value是个Symbol
// 所以一般会使用大型的工具函数:lodash.cloneDeep
  • 不可变数据在 React 中的重要性

为了加速了diff 算法中 reconcile (调和)的过程,React 只需要检查 object 的索引有没有变即可确定数据有没有变

举个

在 React 的生命周期中每次调用 ComponentShouldUpdate() 会将 state 现有的数据跟将要改变的数据进行比较(只会对 state 进行浅对比,也就是更新某个复杂类型数据时只要它的引用地址没变,那就不会重新渲染组件)。

const [todos, setTodos] = useState([{study:'open',,work:'down'}]); 
const onClick = () => { 
    todos[0].study = 'down'; 
    setTodos(todos);
// 不会触发渲染
// 正确的做法
const onClick = () => { 
    let list =[...todos]
    list[0].study='down'
    setTodos(list);
//引入immer
setState(produce((state) => (state.isShow = true)))

四、 常见的实现方法

4.1 深拷贝

深拷贝的成本比较高,需要考虑其他如原型链、 value symbol 或者出现循环引用的处理且没有地址共享的数据,影响性能。

4.2 Immutable.js

Immutable.js 源自 Facebook ,一个非常棒的不可变数据结构的库。使用另一套数据结构的 API,将所有的原生数据类型转化成 Immutable.js 的内部对象,并且任何操作最终都会返回一个新的 Immutable

// 举个 
const { fromJS } = require('immutable')
const data = {
  val: 1,
  desc: {
    text: 'a',
const a = fromJS(data)
const b = a.set('val', 2)
console.log(a.get('val')) // 1
console.log(b.get('val')) // 2
const pathToText = ['desc', 'text']
const c = a.setIn([...pathToText], 'c')
console.log(a.getIn([...pathToText])) // 'a'
console.log(c.getIn([...pathToText])) // 'c'
console.log(b.get('val') === a.get('val'))       // false
console.log(b.get('desc') === a.get('desc')) // true
const d = b.toJS()
const e = a.toJS()
console.log(e.desc === d.desc)       // false
console.log(e.val === d.val) // false

这个例子也可以看出:深层次的对象在没有修改的情况仍然能保证严格相等。这也是它另外一个特点:深层嵌套对象的结构共享

相比与 Immer.js,Immutable.js 的不足:

  • 自己维护一套数据结构、JavaScript 的数据类型和 Immutable.js 需要相互转换,有入侵性
  • 他的操作结果需要通过 toJS 方法才能得到原生对象,这样导致在开发中需要时刻关注操作的是原生对象还是 Immutable.js 返回的结果
  • 库的体积大约在 63KB、而 Immer.js 仅有12KB
  • API 丰富、学习成本较高

五、 Immer.js

基本概念

  • currentState :被操作对象的最初状态
  • draftState : 根据 currentState 生成的草稿、是 currentState 的代理、对 draftState 所有的修改都被记录并用于生成 nextState 。在此过程中, currentState 不受影响
  • nextState : 根据 draftState 生成的最终状态
  • produce : 用于生成 nextState 或者 producer 的函数
  • Producer : 通过 produce 生成,用于生产 nextState ,每次执行相同的操作
  • recipe :用于操作 draftState 的函数

API简介

produce

  • 第一种用法:

produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState

import produce from 'immer'; 
const baseState= [
       title:'study javascript',
       status:true
       title:'study immer'.
       status:false
const nextState = produce(baseState, draftState=>{
    draftState.push({title:'study react'})
    draftState[1].status = true
// 新增的只会体现在在nextState上,baseState没被修改
expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)
expect(baseState[1].status).toBe(false)
expect(nextState[1].status).toBe(true)
// 没有改变的数据共享
expect(nextState[0]).toBe(baseState[0])
// 改变的数据不再共享
expect(nextState[1]).not.toBe(baseState[1])

在上面的例子,对 draftState 的修改最终都会体现在 nextState ,但并不会修改 baseState ,需要注意的是 nextState baseState 共享未修改的部分。需要注意的是通过 produce 生成的 nextState 是被冻结的(使用 Object.freeze 实现,仅冻结 nextState currentState 相比更改的部分),直接修改 nextstate 会报错

  • 第二种用法:柯里化

produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState

利用高阶函数特点,提前生成一个生产者 producer

recipe 没有返回值, nextState 是根据 recipe 函数中的 draftState 生成的;有返回值是根据返回值生成的。

const currentState = {
    x: [5],
let producer = produce((draft) => {
  draft.x = 2
let nextState = producer(currentState);

怎么工作的

produce(obj, draft => {
  draft.count++

通过以上的例子可以看出, obj 是我们传入的简单对象,所以 Immer.js 的神奇一定在 draft 对象上。

核心实现是利用ES6的 proxy 实现 JavaScript 的不可变结构。几乎以最小成本实现了不可变数据结构,简单易用、体量小巧。其基本思想在于所有的更改都应用在临时的 draftState 。一旦完成所有的变更,将草稿状态的变更生成 nextState 。这就通过简单的修改数据同时保留不可变数据的优点。




源码解析

补充知识:Proxy 对象

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

Proxy 对象接受两个参数,第一个参数是需要操作的对象,第二个参数是设置对应拦截的属性,这里的属性同样也支持 get set 等等,也就是劫持了对应元素的读和写,能够在其中进行一些操作,最终返回一个 Proxy 对象实例。

const handle = {
    get(target, key) {
      // 这里的 target 就是 Proxy 的第一个参数对象
      console.log('proxy get key', key)
      return '返回1'
    set(target, key, value) {
      console.log('proxy set key', value)
  const target = {a:{b:1}}
  const p = new Proxy(target,handle)
  p.a = 2 // 所有设置操作都被转发到了 set 方法内部
  p.a.b= 1  // 触发的是get而非set

注意⚠️⚠️⚠️:如果一个对象的层级比较深,而且内部会有引用类型的属性值时。如果给当前对象生成代理并修改内层属性值时,如果修改的是最外层属性的值时,是会触发 set 方法,但是如果修改最外层某个属性值为对象的属性的值时,并不会触发 set 方法

这也就是为什么在 Immer.js 的实现里 需要递归给某个对象内部所有的属性(属性值为对象类型的属性)做代理的原因

Produce的实现

核心源码

produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
        // base判断 是否能生成draft
        if (typeof base === "function" && typeof recipe !== "function") {
            const defaultBase = recipe
            recipe = base
            const self = this
            return function curriedProduce(
                this: any,
                base = defaultBase,
                ...args: any[]
                return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args)) // prettier-ignore
        //  recipe、patchListener异常处理
        if (typeof recipe !== "function") die(6)
        if (patchListener !== undefined && typeof patchListener !== "function")
            die(7)
        let result
        // Only plain objects, arrays, and "immerable classes" are drafted.
        if (isDraftable(base)) {
            // 生成ImmerScope对象,和当前的produce绑定 主要是做复杂嵌套的追踪
            const scope = enterScope(this)
            // 创建 proxy(draft),并执行scope.drafts.push(proxy)将 proxy 保存到 scope 里
            const proxy = createProxy(this, base, undefined)
            let hasError = true
            try {
                // 执行用户的修改逻辑  也就是draftState
                result = recipe(proxy)
                hasError = false
            } finally {
                // finally instead of catch + rethrow better preserves original stack
                if (hasError) revokeScope(scope)
                else leaveScope(scope)
            if (typeof Promise !== "undefined" && result instanceof Promise) {
                return result.then(
                    result => {
                        usePatchesInScope(scope, patchListener)
                        return processResult(result, scope)
                    error => {
                        revokeScope(scope)
                        throw error
            usePatchesInScope(scope, patchListener)
            return processResult(result, scope)
        } else if (!base || typeof base !== "object") {
            result = recipe(base)
            if (result === NOTHING) return undefined
            if (result === undefined) result = base
            if (this.autoFreeze_) freeze(result, true)
            return result
        } else die(21, base)

Produce简单的流程图




总结 produce

produce 接受三个参数: base 初始值、 recipe 用户执行修改逻辑、 patchlistener 用户接受 patch 数据做自定义操作

去除一些特殊的判断兼容处理代码,可以看出其主流程主要根据 base 创建 draft 对象、执行用户传入的 recipe 拦截读写操作,走到自定义的 getter setter 最后再解析组装结果返回给用户。

step1、调用 createProxy 创建 draftState

export function createProxy<T extends Objectish>(
    immer: Immer,
    value: T,
    parent?: ImmerState
): Drafted<T, ImmerState> {
    const draft: Drafted = isMap(value)
        ? getPlugin("MapSet").proxyMap_(value, parent)
        : isSet(value)
        ? getPlugin("MapSet").proxySet_(value, parent)
        : immer.useProxies_
        ? createProxyProxy(value, parent)
        : getPlugin("ES5").createES5Proxy_(value, parent)



这里兼容了不支持 proxy 的ES5处理,其核心根据 base 构建一个 state 对象,如果 base 为数组,是则基于 arrayTraps 创建 state 的Proxy,否则基于 objectTraps 创建 state Proxy

step2、拦截读写操作

export const objectTraps: ProxyHandler<ProxyState> = {
    get(state, prop) {
        if (prop === DRAFT_STATE) return state
        const source = latest(state)
        if (!has(source, prop)) {
            return readPropFromProto(state, source, prop)
        const value = source[prop]
        if (state.finalized_ || !isDraftable(value)) {
            return value
        if (value === peek(state.base_, prop)) {
            prepareCopy(state)
            return (state.copy_![prop as any] = createProxy(
                state.scope_.immer_,
                value,
                state
        return value
        state: ProxyObjectState,
        prop: string /* strictly not, but helps TS */,
        value
        const desc = getDescriptorFromProto(latest(state), prop)
        if (desc?.set) {
            desc.set.call(state.draft_, value)
            return true
        if (!state.modified_) {
            const current = peek(latest(state), prop)
            const currentState: ProxyObjectState = current?.[DRAFT_STATE]
            if (currentState && currentState.base_ === value) {
                state.copy_![prop] = value
                state.assigned_[prop] = false
                return true
            if (is(value, current) && (value !== undefined || has(state.base_, prop)))
                return true
            prepareCopy(state)
            markChanged(state)
            state.copy_![prop] === value &&
            // special case: NaN
            typeof value !== "number" &&
            // special case: handle new props with value 'undefined'
            (value !== undefined || prop in state.copy_)
            return true
        state.copy_![prop] = value
        state.assigned_[prop] = true
        return true





  • getter 主要用来懒初始化代理对象,当代理对象的属性被访问的时候才会生成其代理对象
    • 举个例子:当访问 draft.a 时,通过自定义 getter 生成 draft.a 的代理对象 darftA 所用访问 draft.a.x 相当于 darftA.x ,同时如果 draft.b 没有访问,也不会浪费资源生成 draftB
  • setter :当对 draft 对象发生修改,会对 base 进行浅拷贝保存到 copy 上,同时将 modified 属性设置为 true ,更新在 copy 对象上

step3、解析结果

export function processResult(result: any, scope: ImmerScope) {
    scope.unfinalizedDrafts_ = scope.drafts_.length
    const baseDraft = scope.drafts_![0]
    const isReplaced = result !== undefined && result !== baseDraft
    if (!scope.immer_.useProxies_)
        getPlugin("ES5").willFinalizeES5_(scope, result, isReplaced)
    if (isReplaced) {
        //虽然 Immer 的 Example 里都是建议用户在 recipe 里直接修改 draft,但用户也可以选择在 recipe 最后返回一个 result,不过得注意“修改 draft”和“返回新值”这个两个操作只能任选其一,同时做了的话processResult函数就会抛出错误
        if (baseDraft[DRAFT_STATE].modified_) {
            revokeScope(scope)
            die(4)
        if (isDraftable(result)) {
            //核心处理
            result = finalize(scope, result)
            if (!scope.parent_) maybeFreeze(scope, result)
        if (scope.patches_) {
            getPlugin("Patches").generateReplacementPatches_(
                baseDraft[DRAFT_STATE],
                result,
                scope.patches_,
                scope.inversePatches_!
    } else {
        // Finalize the base draft.
        result = finalize(scope, baseDraft, [])
    revokeScope(scope)
    if (scope.patches_) {
        scope.patchListener_!(scope.patches_, scope.inversePatches_!)
    return result !== NOTHING ? result : undefined
function finalize(rootScope: ImmerScope, value: any, path?: PatchPath) {
    // Don't recurse in tho recursive data structures
    if (isFrozen(value)) return value
    const state: ImmerState = value[DRAFT_STATE]
    // A plain object, might need freezing, might contain drafts
    if (!state) {
        each(
            value,
            (key, childValue) =>
                finalizeProperty(rootScope, state, value, key, childValue, path),
            true // See #590, don't recurse into non-enumarable of non drafted objects
        return value
    // Never finalize drafts owned by another scope.
    if (state.scope_ !== rootScope) return value
    // Unmodified draft, return the (frozen) original
    if (!state.modified_) {
        maybeFreeze(rootScope, state.base_, true)
        return state.base_
    // Not finalized yet, let's do that now
    if (!state.finalized_) {
        state.finalized_ = true
        state.scope_.unfinalizedDrafts_--
        const result =
            // For ES5, create a good copy from the draft first, with added keys and without deleted keys.
            state.type_ === ProxyType.ES5Object || state.type_ === ProxyType.ES5Array
                ? (state.copy_ = shallowCopy(state.draft_))
                : state.copy_
        // Finalize all children of the copy
        // For sets we clone before iterating, otherwise we can get in endless loop due to modifying during iteration, see #628
        // Although the original test case doesn't seem valid anyway, so if this in the way we can turn the next line
        // back to each(result, ....)
        each(
            state.type_ === ProxyType.Set ? new Set(result) : result,
            (key, childValue) =>
                finalizeProperty(rootScope, state, result, key, childValue, path)
        // everything inside is frozen, we can freeze here
        maybeFreeze(rootScope, result, false)
        // first time finalizing, let's create those patches
  • 解析结果:其核心在于 result = finalize(scope, baseDraft, []) 。当 produce 执行完成,所有的用户修改也完成。
    • 如果 state.modified_=false 未被标记修改:说明没有更改该对象,直接返回原始 base
    • 如果 state.finalized_=false 未被标记结束:递归 base copy 的子属性执行 finalizeProperty ,如果相同则返回,否则递归整个过程。最后返回的对象是由 base 的一些没有修改的属性和 copy 修改的属性拼接而成,最终使用 freeze 冻结 copy 属性,将 finalized 设为 true

六、Immer.js 在 React 项目中的优势

setState 中使用 Immer.js

onClick = () => { 
    this.setState(prevState => ({ 
        user: { 
            ...personState.user, 
            age: personState.user.age + 1 
// 使用immer
onClickImmer = () => { 
    this.setState(produce(draft=>{
        draft.user.age +=1    

​以 hook 的方式使用 Immer.js

Immer.js 还提供了一个 React Hook 库 use-Immer ,可以在React的项目中以 hook 的形式使用 Immer

useImmer useState 非常像。它接收一个初始状态,返回一个数组。数组第一个值为当前状态,第二个值为状态更新函数。状态更新函数和 produce 中的 recipe 一样。举个 :

import React from "react"
import { useImmer } from "use-immer"
const [person, updatePerson] = useImmer({name:'tom',age:25})