ImmerJs使用详解

2,961 阅读5分钟

ImmerJs源码解析: juejin.cn/post/703990…

久闻ImmerJS的大名,今天上班没事干,摸鱼来学习一下

当前使用的ImmerJS版本为 9.0.6(2021年10月27号)

$ yarn add immer@9.0.6

ImmerJS使用proxy实现了不可变数据类型,相对比 ImmutableJs而言,由于不需要内部实现一套数据结构,所以少了很多概念,api与体积(ImmutableJs是巨大的,60KB左右)

1 入门使用

1.1 对比

现在准备一个基础的state

const baseState = [
    {
        title: "Learn TypeScript",
        done: true
    },
    {
        title: "Try Immer",
        done: false
    }
]

现在,我们想要把baseState的第二项done改为true,并添加一个新的项,并把这个值存储在新的 nextState 中(避免baseState的值发生变化)

接下来就看看下面三种情况下如何实现这个功能

1.1.1 原生js

// 原生js
const nextState = [...baseState]
nextState[1] = {
  ...nextState[1],
  done: true
}
nextState.push({
  title: 'React18 is coming',
  done: false
})

console.log(JSON.stringify(baseState, null, 2))
console.log(JSON.stringify(nextState, null, 2))

这样就得到了两棵数据结构完成不同的树

image-20211027094855343.png

1.1.2 ImmutableJs

先安装一下immutableJs

$ yarn add immutable@4.0.0

使用如下

import { List } from 'immutable'

// 把数组转换成immutableJs内部结构
const _baseState = List(baseState)
// immutableJs每次更改都会返回一个新的对象
const array2 = _baseState.push({
  title: 'React18 is coming',
  done: false
})
const nextState = array2.update(1, oldValue=>{
  // 这里本质上,还是得使用原生方法进行结构
  return {
    ...oldValue,
    done: true
  }
})

console.log(JSON.stringify(_baseState.toArray(), null, 2))
console.log(JSON.stringify(nextState.toArray(), null, 2))

image-20211027094855343.png

1.1.3 ImmerJs

import produce from 'immer'

const nextState = produce(baseState, draft => {
    draft[1].done = true
    draft.push({ title: 'React18 is coming', done: false })
})

console.log(JSON.stringify(baseState, null, 2))
console.log(JSON.stringify(nextState, null, 2))

image-20211027094855343.png

ImmerJs也不会改变没有修改数据的地址指向

console.log(nextState[0] === baseState[0])   // true

1.2 ImmerJs扩展

ImmerJs本身体积只有 ImmutableJs1/8左右,但 ImmerJs本身只能实现数组和对象的监控,对于 MapSet ImmerJs无法实现监控,或者想要兼容es5,这就可以开启对应的功能

import produce, { enableES5, enableMapSet, enablePatches, enableAllPlugins } from 'immer'

enableES5()
enableMapSet()
enablePatches()
enableAllPlugins()
新旧技术支持使用体积
-produce7.83KB
兼容es5enableES5()+ 1.7KB
MapSet 响应支持enableMapSet()+ 2.83KB
数据变化监听, 见 (3 跟踪变化)enablePatches()+ 2.33KB
获取全部功能支持enableAllPlugins()+ 6.87KB

2 draft与current/original

2.1 draft销毁问题

使用produce代理数据的时候,在当前执行栈中,draft可以随意访问与修改,如下

const base = {
  x: 0
}

produce(base, draft=>{

  console.log(draft.x)      // 0
  ...
  ...

})

但当produce执行完成之后,draft就会被销毁

produce(base, draft=>{

  console.log(draft.x)      // 0

  setTimeout(()=>{
    console.log(draft.x)    // Uncaught TypeError: 无法对已撤销的代理执行“get”操作
  }, 100)

})

2.2 current

ImmerJs提供了current api,能够复制一份draft,这样就能在另外一个执行栈获取当时的数据

import produce, { current } from 'immer'

const base = {
  x: 0
}

produce(base, draft=>{

  draft.x++

  const copy = current(draft)

  draft.x++

  console.log(copy.x)		// 1
  console.log(draft.x)		// 2

  setTimeout(()=>{
    console.log(copy.x)    // 1
  }, 100)

})

2.3 original

ImmerJs提供了original api,能够获取draft修饰的原来的数据,类似于vue3toRaw

import produce, { original, current } from 'immer'

const base = {
  x: 0
}

produce(base, draft=>{

  draft.x++

  console.log(original(draft) === base)		// true

})

3 跟踪变化

redux能够在浏览器扩展中监听数据的变化,原理是借助中间件监听数据变化,ImmerJs也能监听draft的变化。前提是需要开启此功能

import { enablePatches } from 'immer'

enablePatches()

3.1 produce监听变化

开启pathes功能后,可以在produce添加第三个参数

type State = {
  name?: string,
  age: number,
  attr: number[]
  addAttr?: string
}

// 1 创建一个初始state
const state: State = {
  name: "name",
  age: 1,
  attr: []
}

// 用来保存从 state => nextState 的步骤
const changes = []
// 用来保存从 nextState => state 的步骤
const inverseChanges = []

const nextState = produce(
  state,
  draft => {
    // 修改元素
    draft.age = 33
    // 添加属性
    draft.addAttr = 'addAttr'
    // 删除属性
    delete draft['name']
    // 添加元素
    draft.attr[1] = 1
  },
  // 此函数只会调用一次
  (patches, inversePatches) => {
    changes.push(...patches)
    inverseChanges.push(...inversePatches)
  }
)

console.log(JSON.stringify(changes, null, 2))
console.log(JSON.stringify(inverseChanges, null, 2))

可以看到的是,changes中,保存这从state转化为nextState的步骤

image-20211027145140368.png

inverseChanges中保存这从nextState还原到state的步骤

image-20211027145343474.png

3.2 produceWithPatches

专门写一个函数用于存储changes略显麻烦,ImmerJs提供了一个全新的api,能够一次性返回nextStatechangesinverseChanges

import { produceWithPatches, enablePatches } from 'immer'

enablePatches()

const state = {
  age: 1
}

const [ nextState, changes, inverseChanges ] = produceWithPatches(state, (draft)=>{
    draft.age++
})

3.3 applyPatches

拥有了patches,开发者可以使用这些pathes转化为任意阶段的类型,ImmerJs提供了 applyPatches 这个全新的api来帮助开发这快速转换

import { produceWithPatches, enablePatches, applyPatches } from 'immer'

enablePatches()

const state = {
  age: 1
}

const [ nextState, changes, inverseChanges ] = produceWithPatches(state, (draft)=>{
    draft.age++
})

// 把 nextState 转换为 state
console.log(applyPatches(nextState, inverseChanges))  // { age: 1 }
// 把 state 转换为 nextState
console.log(applyPatches(state, changes))             // { age: 2 }

4 数据不可变性质

经过ImmerJs修饰的数据,返回的值是不可修改的,类似于 Obejct.freeze,但Obejct.freeze只会修饰对象的第一层为不可变,而ImmerJs返回的值是全部不可修改,想要更改,仍然需要借助produce进行修改

import produce from 'immer'

const state = {
  age: {
    value: 1
  },
  name: {
    value: 1
  }
}

const nextState = produce(state, (draft) => {
  draft.age.value++
})

nextState.name.value++  // Uncaught TypeError: Cannot assign to read only property 'value' of object '#<Object>'
nextState.age.value++   // Uncaught TypeError: Cannot assign to read only property 'value' of object '#<Object>'

但是,可以使用ImmerJs提供的 setAutoFreeze api ,来关闭数据冻结

import produce, { setAutoFreeze } from 'immer'

// 关闭数据冻结
setAutoFreeze(false)
// 开启数据冻结
// setAutoFreeze(true)

const state = {
  age: {
    value: 1
  },
  name: {
    value: 1
  }
}

const nextState = produce(state, (draft) => {
  draft.age.value++
})

nextState.name.value++
nextState.age.value++

console.log(nextState)  //  { age: {value: 3}, name: {value: 2} }

5 produce

5.1 回调返回值

produce的第一个回调函数中,可以直接返回一个值

  • 如果这个值为undefined,那么返回的值就是 state
  • 如果这个值不为undefined,那么返回的值就是return出来的值(不可变)
import produce from "immer"

const baseState = {
  list: [
    { name: '1', age: 18 },
    { name: '2', age: 19 },
    { name: '3', age: 20 },
    { name: '4', age: 21 },
    { name: '5', age: 22 }
  ]
}

const nextState = produce(baseState, draft => {
  return draft.list.filter(t => t.age % 2)
})

console.log(nextState)
// [
//    {name: "2", age: 19},
//    {name: "4", age: 21}
// ]

如果,就是想返回undefined,可以return ImmerJs提供的一个 nothing

import produce, { nothing } from "immer"

const state = {
  age: 1
}

const nextState = produce(state, draft => {
  return nothing
})

console.log(nextState)   // undefined

5.2 一行写法

由于produce的这个5.1特性,所以如果只想修改某一个元素,并且此方法只想写一行,就会出现问题

const baseState = {
  age: 1
}

const nextState = produce(baseState, draft => draft.age = 2)

image-20211027155138198.png

可以采用如下方法

const baseState = { age: 1 }

const nextState = produce(baseState, draft => void (draft.age = 2))

console.log(nextState)   // { age: 2 }

多行操作如下

const baseState = { age: 1 }

const nextState = produce(baseState, draft => void ((draft.age++), (draft.age++)))

console.log(nextState)   // { age: 3 }

5.3 异步produce

produce还能传入异步方法,比如,现在准备一个node服务器

const { createServer } = require('http')

const server = createServer(function (request, response) {
  response.setHeader('Access-Control-Allow-Origin', request.headers.origin)
  response.setHeader('Access-Control-Allow-Headers', 'content-type')
  response.setHeader('Access-Control-Allow-Methods', 'DELETE,PUT,POST,GET')
  response.setHeader('Content-Type', 'application/json;charset=utf-8')
  response.end(JSON.stringify([
    { name: '1', age: 1 },
    { name: '2', age: 2 },
    { name: '3', age: 3 },
    { name: '4', age: 4 }
  ]))
})

// 启动服务
server.listen(3000, function (err) {
  if (err) {
    console.log('error', err)
  } else {
    console.log('服务器启动成功 http://127.0.0.1:3000')
  }
})

如下,如果produce发现回调函数中返回的是一个Promise,那么produce也会返回一个Promise

import produce from "immer"

const baseState = { list: [] }

async function my(){
  const nextState = await produce(baseState, async draft => {
    draft.list = await (await fetch('http://127.0.0.1:3000')).json()
  })
}

my()

6 createDraft/finishDraft

可以不使用 produceproduceWithPatches 实现数据变化,ImmerJs还提供了两个api实现此功能

import { createDraft, finishDraft } from "immer"

const baseState = { age: 1 }
const draft = createDraft(baseState)
draft.age++
const nextState = finishDraft(draft)

console.log(baseState)    // { age: 1 }
console.log(nextState)    // { age: 2 }