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))
这样就得到了两棵数据结构完成不同的树
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))
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))
ImmerJs
也不会改变没有修改数据的地址指向
console.log(nextState[0] === baseState[0]) // true
1.2 ImmerJs扩展
ImmerJs
本身体积只有 ImmutableJs
的 1/8
左右,但 ImmerJs
本身只能实现数组和对象的监控,对于 Map
和 Set
ImmerJs
无法实现监控,或者想要兼容es5
,这就可以开启对应的功能
import produce, { enableES5, enableMapSet, enablePatches, enableAllPlugins } from 'immer'
enableES5()
enableMapSet()
enablePatches()
enableAllPlugins()
新旧技术支持 | 使用 | 体积 |
---|---|---|
- | produce | 7.83KB |
兼容es5 | enableES5() | + 1.7KB |
Map 与Set 响应支持 | 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
修饰的原来的数据,类似于vue3
的toRaw
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
的步骤
inverseChanges
中保存这从nextState
还原到state
的步骤
3.2 produceWithPatches
专门写一个函数用于存储changes
略显麻烦,ImmerJs
提供了一个全新的api
,能够一次性返回nextState
,changes
和inverseChanges
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)
可以采用如下方法
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
可以不使用 produce
或 produceWithPatches
实现数据变化,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 }