这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战
在讲解 immer 高性能的原因之前,首先要普及一下 immer 的背景知识。
immerjs
是一个 基于 immutablejs
思想的一个库,用于解决在 redux 的 reducer 中处理数据异常复杂的问题。
下面我们通过一个例子看看 传统 redux 中 reducer 是怎么书写的:
const initialState = {
value: 0
}
// reducer.js
export default function reducer(state, action) {
const { type, payload } = action
switch(type) {
case 'add':
return {
...state,
value: state.value + payload
}
}
}
这个 reducer 用于对 value 进行累加操作。看起来好像不是很麻烦,那么我们换一个更贴近业务逻辑的例子。
const initialState = {
user: {
name: '',
address: {
home: '',
company: '',
}
}
}
// reducer.js
export default function reducer(state, action) {
const { type, newHomeAddress } = action
switch(type) {
case 'update-home-address':
return {
...state,
address: {
...state.address,
home: newHomeAddress,
}
}
}
}
从这个例子中,可以看到当 redux 中 state 的嵌套层级很深的时候,在处理 reducer 的返回数据逻辑时冗长而丑陋。
immer
就是为了解决这个问题而诞生的。
既然我们是想把 user.address.home
修改为 newHomeAddress
,为什么我们需要不断的解构来实现呢,可不可以直接声明 user.address.home = newHomeAddress
呢。
当然是可以的。我们知道只需要把 对象经过 Proxy 代理一下,就可以解决这个问题。类似于 vue 中响应式的实现方式。
immer
也确实是这么做的,但在此基础上,可能又会产生新的问题。如果这个 user 下嵌套的层级非常深,我们一开始进行代理的时候去循环代码它的内层对象,就可能导致性能问题。如果不深层代理,就肯定会出现内层属性没有代理成功,从而导致 bug。
immer
是怎么解决这个问题的呢,这就是本篇文章要讲的重点。
immer
是通过 懒代理的方式去实现高性能的数据代理的。
懒代理,即用到的时候才代理。我们举个例子说明一下:
const user = {
address: {
home: '',
}
}
如上这个user,immer
只会对 user 进行代理,当访问 user 时会触发 user 的 getter 方法,进而 immer
会对 user.address
也进行代理,以此类推,只有你访问到这个对象时,immer
才会对它内层的属性再次创建 Proxy
代理,这样就巧妙的解决了层层代理的性能问题。
先来使用 immer
改造一下上面的 reducer:
import produce from 'immer'
const initialState = {
user: {
name: '',
address: {
home: '',
company: '',
}
}
}
// reducer.js
export default produce(function reducer(state, action) {
const { type, newHomeAddress } = action
switch(type) {
case 'update-home-address':
state.address.home = newHomeAddress
break
}
}, initialState)
可以看到,直接对 home 赋值的一行代码就实现了之前层层解构合并的功能。
下面我们看看源码中是怎么实现的。(略去次要部分)
function produce(base, recipe) {
const proxy = createProxy(this, base, undefined)
}
function createProxy(immer, value, parent) {
// only Array or Object run this
const draft = createProxyProxy(value, parent)
return draft
}
可以看到,传入 produce 的值,开始就做了代理相关的工作,createProxyProxy,我们展开看一下
function createProxyProxy(base, parent) {
const state: ProxyState = {
type_: isArray ? ProxyTypeProxyArray : (ProxyTypeProxyObject as any),
scope_: parent ? parent.scope_ : getCurrentScope()!,
modified_: false,
finalized_: false,
assigned_: {},
parent_: parent,
base_: base,
draft_: null as any,
copy_: null,
revoke_: null as any,
isManual_: false
}
let traps = objectTraps
const { proxy } = Proxy.revocable(state, traps)
state.draft = proxy
state.revoke_ = revoke
return proxy
}
首先创建了一个 state,这个对象上有很多属性,包括 需要代理的值 state,类型,父scope,是否已经结束,是否修改等等。
然后通过 Proxy.revocable
对这个 state 做了代理,与 new Proxy 不同的是,通过这种方式创建的代理是可以 revoke
的。
具体 revoke 的作用后文在分析。
可以看到,这里的关键点就在于 obejctTraps
,因为代理的具体 get、set 还没有展开说。
const objectTraps = {
get(state, prop) {
const value = source[prop]
if (!isDraftable(value)) return value
prepareCopy(state)
return (
state.copy_[prop] = createProxy(
state.scope_.immer_,
value,
state
)
)
}
}
从这里可以看到,首先 immer
会拿到开发者想要取到的真实的值,然后会判断 isDraftable
,这个方法其实是为了区分是否是基本数据类型,因为一些基本数据类型是不需要被代理的,所以直接返回。
这也就是 immer
在执行懒代理时,不会对所有内层属性都代理,只有这个内层属性是有数组、对象、Map、Set 等能够被代理的值才会代理。
然后 prepareCopy 的代码:
export function prepareCopy(state: {base_: any; copy_: any}) {
if (!state.copy_) {
state.copy_ = shallowCopy(state.base_)
}
}
可以看到 prepareCopy 在 state 上绑定了一个原本数据的浅拷贝。
为什么要这么做呢,因为 immer
在懒代理的时候当然不能直接对访问的属性再次代理,因为这样的话 就是全量代理了。那么他先创建一个副本在 copy 属性上,然后当真正需要创建内层代理的时候,再对这个 copy 进行代理即可。
可以看到 prepareCopy 后面的代码,它分为两部分:
state.copy_[prop] = createProxy(
state.scope_.immer_,
value,
state
)
return state.copy_[prop]
首先,对这个需要被代理的值创建了一个代理 放到了 copy[prop] 上。然后返回了这个 代理对象。
这样就保证了,当再次访问 深层属性的时候,可以被刚刚这个 state.copy_[prop] 的代理 getter 捕获到,如果深层又可以代理,再次重复 creteProxy 的逻辑。
至此,就实现了 immer 中对对象的懒代理,保证了代理的高性能。