immer是前端在immutable领域的另外一个实践,相比较immutable而言,它拥有更低的学习成本,在使用上可以直接使用js 原生api去修改引用对象,得到一个新的不可变的引用对象。
import produce from "immer"
const baseState = [
{
todo: "Learn typescript",
done: true
},
{
todo: "Try immer",
done: false
}
]
const nextState = produce(baseState, draftState => {
draftState.push({todo: "Tweet about it"})
draftState[1].done = true
})
immer的实现主要有两种方案,在支持Proxy的环境下会使用Proxy,在不支持Proxy的环境下会使用defineProperty。在这里主要介绍Proxy的实现方案,因为基本的实现思路都是相似的,通过学习Proxy的实现方案,我们也能熟悉一下在平时业务开发时很少用的Proxy api。
从上面的例子可以看出使用immer,主要通过调用produce这个api,从源码中可以看到produce这个函数其实调用了produceProxy函数:
function produce(baseState, producer) {
...
return getUseProxies()
? produceProxy(baseState, producer)
: produceEs5(baseState, producer)
}
produceProxy
在继续阅读immer的源码之前,我们不妨想一下,如何通过Proxy实现immer的功能?
我们必须在没修改对象的情况下获取原对象的属性,在修改的情况下又不要修改原对象的属性。我们可以很容易想到get handler的操作:
new Proxy(data, {
get(target, prop){
return target[prop]
},
set(target, prop, value){
}
})
但是set如何处理?所以我们代理的对象不能只是数据本身,在immer中每个代理的对象都是以下结构:
function createState(parent, base) {
return {
base, // 要代理的原数据
parent, // 要代理数据的父对象
copy: undefined, // 在set时,修改这个数据对应的值
proxies: {}, // 讲解get时,再谈这个
modified: false, // 有没有要修改这份数据
finalized: false // 本文最后会讲解
}
}
接下来我们回到produceProxy函数:
export function produceProxy(baseState, producer) {
...
const previousProxies = proxies
proxies = [] // 通过createProxy创建的proxy都会在这里面
try {
// create proxy for root
const rootProxy = createProxy(undefined, baseState) // 创建根代理
// execute the thunk
const returnValue = producer.call(rootProxy, rootProxy) // 执行函数,拿到返回值
// and finalize the modified proxy
let result
// check whether the draft was modified and/or a value was returned
if (returnValue !== undefined && returnValue !== rootProxy) {
...
} else {
result = finalize(rootProxy)
}
// revoke all proxies
each(proxies, (_, p) => p.revoke()) // 销毁代理,主要是为了防止外层的变量拿到这个代理做一些操作
return result
} finally {
proxies = previousProxies
}
}
可以看到主要逻辑还是很清晰的,为数据创建代理,然后调用producer函数,最后finalize(rootProxy)。
接下来看一下createProxy的相关逻辑:
function createProxy(parentState, base) {
if (isProxy(base)) throw new Error("Immer bug. Plz report.")
const state = createState(parentState, base)
const proxy = Array.isArray(base)
? Proxy.revocable([state], arrayTraps)
: Proxy.revocable(state, objectTraps)
proxies.push(proxy)
return proxy.proxy
}
createProxy的逻辑看起来很简单,但是你可能会有两个疑问:
- 为什么使用Proxy.revocable做代理,而不是new Proxy?
- 为什么要把数组的state包裹到一个数组里面
[state]
先来回答第一个问题,使用Proxy.revocable主要是为了防止以下情况的出现:
let proxy
const nextState = produce(baseState, s => {
proxy = s
s.aProp = "hello"
})
proxy.aProp = "Hallo"
如代码所示,如果produce执行完成后,proxy不做revoke,会导致外部变量拿到的proxy,还有作用,就会造成不期望的情况出现。所以在produceProxy最后,会把函数执行周期所有创建的proxy都revoke掉。
第二个问题,通过produceProxy的代码,我们可以看到在调用外部传入的producer函数的时候,传给producer函数的是proxy,如果不使用[state]
,proxy代理的state就是一个对象。此时如果对其类型进行判断Array.isArray(proxy)
就会返回false。
我们可以看一下objectTraps和arrayTraps分别是什么:
const objectTraps = {
get,
has(target, prop) {
return prop in source(target)
},
ownKeys(target) {
return Reflect.ownKeys(source(target))
},
set,
deleteProperty,
getOwnPropertyDescriptor,
defineProperty,
setPrototypeOf() {
throw new Error("Immer does not support `setPrototypeOf()`.")
}
}
const arrayTraps = {}
each(objectTraps, (key, fn) => {
arrayTraps[key] = function() {
arguments[0] = arguments[0][0]
return fn.apply(this, arguments) // state push proxy
}
})
可以看到objectTraps是一个很普通的handlers,而arrayTraps则是在objectTraps上包裹了一层,传入的参数将[state]
改为了state
。
Handler
接下来看一下get,先看一下数据没有被修改过的情况(即还没调用过set)
function get(state, prop) {
if (prop === PROXY_STATE) return state // PROXY_STATE是一个symbol值,有两个作用,一是便于判断对象是不是已经代理过,二是帮助proxy拿到对应state的值
if (state.modified) {
...
} else {
if (has(state.proxies, prop)) return state.proxies[prop]
const value = state.base[prop]
if (!isProxy(value) && isProxyable(value))
return (state.proxies[prop] = createProxy(state, value))
return value
}
}
get函数主要有两个作用:
- 返回对应的数据
- 为对应的数据创建代理
通过get的时候创建代理就保证了不管在produce中操作的数据嵌套有多深,我们操作的都是代理对象,如:
a.b.c = 1 // a.b是一个代理对象
a.b.c.push(1) // a.b.c是一个代理对象
接下来看set函数
function set(state, prop, value) {
// set的关键是不改老的值,所以改的copy上的值
if (!state.modified) {
if (
(prop in state.base && is(state.base[prop], value)) ||
(has(state.proxies, prop) && state.proxies[prop] === value) //值不变的情况下直接return true
)
return true
markChanged(state)
}
state.copy[prop] = value
return true
}
set的逻辑相对简单,set值就是改state.copy上的值,同时如果state是第一次修改,就markChanged(state)
function markChanged(state) {
if (!state.modified) {
state.modified = true
state.copy = shallowCopy(state.base)
// copy the proxies over the base-copy
Object.assign(state.copy, state.proxies) // yup that works for arrays as well
if (state.parent) markChanged(state.parent)
}
}
在markChanged函数中,把base的属性和proxies的上的属性都浅拷贝给了copy,从此,对目标对象的取值还是设值都是操作state.copy。
我们看一下整个get函数,可以看到state.modified为true的情况下,逻辑很简单,对应属性没变化的时候创建代理,返回值,对应属性变化了,直接返回对应值。
function get(state, prop) {
if (prop === PROXY_STATE) return state
if (state.modified) {
const value = state.copy[prop]
if (value === state.base[prop] && isProxyable(value))
return (state.copy[prop] = createProxy(state, value))
return value
} else {
if (has(state.proxies, prop)) return state.proxies[prop]
const value = state.base[prop]
if (!isProxy(value) && isProxyable(value))
return (state.proxies[prop] = createProxy(state, value))
return value
}
}
finalize
除了get和set,还有6个其他的handler,但整体思路和get、set一致,就不一一介绍了。我们看一下produceProxy的最后一块,也是我认为最不好理解的一部分finalize。
export function produceProxy(baseState, producer) {
...
const previousProxies = proxies
proxies = [] // 通过createProxy创建的proxy都会在这里面
try {
// create proxy for root
const rootProxy = createProxy(undefined, baseState) // 创建根代理
// execute the thunk
const returnValue = producer.call(rootProxy, rootProxy) // 执行函数,拿到返回值
// and finalize the modified proxy
let result
// check whether the draft was modified and/or a value was returned
if (returnValue !== undefined && returnValue !== rootProxy) {
...
} else {
result = finalize(rootProxy)
}
// revoke all proxies
each(proxies, (_, p) => p.revoke()) // 销毁代理,主要是为了防止外层的变量拿到这个代理做一些操作
return result
} finally {
proxies = previousProxies
}
}
前面通过producer函数对rootProxy进行了一系列的操作,现在我们要返回下一次的state,我们要递归地state上的属性,把属性对应的代理对象,改为对应的值。
export function finalize(base) {
if (isProxy(base)) {
const state = base[PROXY_STATE]
if (state.modified === true) {
if (state.finalized === true) return state.copy
state.finalized = true
return finalizeObject(
useProxies ? state.copy : (state.copy = shallowCopy(base)),
state
)
} else {
return state.base
}
}
finalizeNonProxiedObject(base) // base不是代理则说明base下面的属性会有代理
return base
}
因为我们第一次传入finalize函数的是rootProxy,是一个Proxy,我们先看isProxy(base)为true的情况,简化一下对应的逻辑,可以看到逻辑很简单:
const state = base[PROXY_STATE]
if (state.modified === true) {
return finalizeObject(
state.copy,
state
)
} else {
return state.base
}
如果state没有被修改过,就直接返回state.base,如果state修改过,就返回finalizeObject(state.copy, state)函数的返回值。
至于为什么要设置state.finalized
的值,我们稍后再讲,我们先看一下finalizeObject函数的逻辑。
function finalizeObject(copy, state) {
const base = state.base
each(copy, (prop, value) => {
if (value !== base[prop]) copy[prop] = finalize(value)
})
return freeze(copy)
}
finalizeObject函数遍历copy上的属性,对于value和base[prop]不相等的情况,调用finalize(value),最后freeze copy对象,然后返回。
value和base[prop]不相等说明可能存在两种情况:
- 由于value被get过,此时value是一个代理对象。
- value被set过,此时value可能是一个普通的值也可能是一个代理对象(比如把rootProxy的某个子孙代理属性赋值给了copy[prop],即value)。
所以我们就好理解finalize函数中为什么既要处理value是proxy的情况,又要处理value不是proxy的情况了。
当value不是一个proxy的时候,value的子属性可能是一个proxy(因为赋值的时候,可能值的子属性是proxy),immer用finalizeNonProxiedObject
处理这种情况。
function finalizeNonProxiedObject(parent) {
// If finalize is called on an object that was not a proxy, it means that it is an object that was not there in the original
// tree and it could contain proxies at arbitrarily places. Let's find and finalize them as well
if (!isProxyable(parent)) return
if (Object.isFrozen(parent)) return
each(parent, (i, child) => {
if (isProxy(child)) {
parent[i] = finalize(child)
} else finalizeNonProxiedObject(child)
})
// always freeze completely new data
freeze(parent)
}
如果属性值是一个proxy,就调用finalize,以去除proxy,否则就递归的去找下面属性是不是proxy。
最后我们说一下finalize函数中为什么要state.finalized = true
,按照正常的逻辑属性在finalize函数中只会访问一次,根本这行代码。
这行代码是为了防止一种情况,某个属性值被赋值给了另外一个属性,这两个属性访问的是一个数据,此时如果state已经finalized,就直接返回他的copy。
至此,我们已经阅读完immer的核心逻辑,和所有比较难以理解的地方。