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 }