immer 原理
前言
掘金上的immer源码解析,根据的是以前的源码,跟现在的源码略有不同,大体思路上是可以参考的。大家自己看的话,最好还是以github上的源码为准,毕竟文章中的源码不知道什么时候就过时了。
下面的涉及的源码是7.0.9版本拉取的。
Proxy
immer 原理涉及到ES6的特性:Proxy
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
如下是对读取属性拦截的示例:
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
Proxy支持的拦截操作:
- get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。
- set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
- has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
- deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
- ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
- getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
- defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
- preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
- getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
- isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
- setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
- apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
- construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
immmer就是利用的Proxy的特性实现的。
immer原理
主要理解的是pruduce这个api,大部分情况下使用这个api就已经可以达到实现不可变数据的要求了。
const obj2=produce(obj, draft => {
draft.count++
})
obj 是个普通对象,immer通过proxy给obj生成了一份草稿draft对象,当你对draft进行操作时,都会被监听,对draft的修改会进入自定义的setter函数。
在setter函数中,它并不会修改原始对象的值,而是递归父级不断拷贝,最终返回新的顶层对象,并作为produce函数的值。
produce
produce 方式其实就是Immer类中的produce方法
produce(base: any, recipe?: any, patchListener?: any) {
// curried invocation
// 若base为函数,则返回一个调用函数curriedProduce,
// curriedProduce进行正常调用produce,即base为对象,recipe为函数
if (typeof base === "function" && typeof recipe !== "function") {
const defaultBase = recipe
recipe = base
const self = this
return function curriedProduce(
this: any,
base = defaultBase,
...args: any[]
) {
return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args)) // prettier-ignore
}
}
//下面是base 为对象,recipe为函数, produce的主流程
//
if (typeof recipe !== "function") die(6)
if (patchListener !== undefined && typeof patchListener !== "function")
die(7)
let result
// Only plain objects, arrays, and "immerable classes" are drafted.
// 只有base 是对象 ,数组,才会生成草稿immerable classes
//判断base 是否能被草稿化
if (isDraftable(base)) {
//对currentScope进行初始化
/**scope的数据格式:
* {
drafts_: [],
parent_,
immer_,
// Whenever the modified draft contains a draft from another scope, we
// need to prevent auto-freezing so the unowned draft can be finalized.
canAutoFreeze_: true,
unfinalizedDrafts_: 0
}
*/
const scope = enterScope(this)
// 获取基于base的代理对象,用于监听处理 后续recipe函数操作并生成不可变对象
const proxy = createProxy(this, base, undefined)
let hasError = true
try {
//执行函数,一般回调函数不返回数据,所以result一般为undefined
result = recipe(proxy)
hasError = false
} finally {
// finally instead of catch + rethrow better preserves original stack
if (hasError) revokeScope(scope)
else leaveScope(scope)
}
if (typeof Promise !== "undefined" && result instanceof Promise) {
//对于Promise 类型的处理
return result.then(
result => {
usePatchesInScope(scope, patchListener)
return processResult(result, scope)
},
error => {
revokeScope(scope)
throw error
}
)
}
usePatchesInScope(scope, patchListener)
//解析并返回结果数据
return processResult(result, scope)
} else if (!base || typeof base !== "object") {
result = recipe(base)
if (result === NOTHING) return undefined
if (result === undefined) result = base
if (this.autoFreeze_) freeze(result, true)
return result
} else die(21, base)
}
- enterScope 生成当前全局的scope,后续用于保存draft,可以调用getCurrentScope来获取全局scope,进入获取draft
- 调用createProxy,获取基于base的代理,从而生成draft,实现监听和处理相关的数据操作。
- 调用recipe,实现用户的数据操作,返回result
- 调用processResult来返回最终修改完成的不可变数据
enterScope
export function enterScope(immer: Immer) {
// createScope返回scope,并赋值给全局currentScope
return (currentScope = createScope(currentScope, immer))
}
function createScope(
parent_: ImmerScope | undefined,
immer_: Immer
): ImmerScope {
//返回 immerscope对象
return {
drafts_: [],
parent_,
immer_,
// Whenever the modified draft contains a draft from another scope, we
// need to prevent auto-freezing so the unowned draft can be finalized.
// 只要修改后的draft包含来自其他作用域的draft,我们需要在最终draft确定前,防止其自动冻结。
canAutoFreeze_: true,
unfinalizedDrafts_: 0
}
}
createProxy
export function createProxy<T extends Objectish>(
immer: Immer,
value: T,
parent?: ImmerState
): Drafted<T, ImmerState> {
// precondition: createProxy should be guarded by isDraftable, so we know we can safely draft
// 前提条件: createProxy 应该有 isDraftable函数判断保护,所以我们才能在草稿进行安全地操作。
//下面是value ,即传入produce的base的数据类型各自对于的处理
const draft: Drafted = isMap(value)
? getPlugin("MapSet").proxyMap_(value, parent)
: isSet(value)
? getPlugin("MapSet").proxySet_(value, parent)
: immer.useProxies_ //判断是否支持 ES6的Proxy
? createProxyProxy(value, parent) //** 支持es6 Proxy,生成draft
: getPlugin("ES5").createES5Proxy_(value, parent) //不支持,采用自带的es5"Proxy"
const scope = parent ? parent.scope_ : getCurrentScope()
scope.drafts_.push(draft)
return draft
}
export function createProxyProxy<T extends Objectish>(
base: T,
parent?: ImmerState
): Drafted<T, ProxyState> {
const isArray = Array.isArray(base)
//初始化 base 的状态,用于记录后续数据操作的一些变化
const state: ProxyState = {
type_: isArray ? ProxyTypeProxyArray : (ProxyTypeProxyObject as any),
// Track which produce call this is associated with.
scope_: parent ? parent.scope_ : getCurrentScope()!,
// True for both shallow and deep changes.
modified_: false,
// Used during finalization.
finalized_: false,
// Track which properties have been assigned (true) or deleted (false).
assigned_: {},
// The parent draft state.
parent_: parent,
// The base state.
base_: base,
// The base proxy.
draft_: null as any, // set below
// The base copy with any updated values.
copy_: null,
// Called by the `produce` function.
revoke_: null as any,
isManual_: false
}
let target: T = state as any
// 默认捕获器 是obj类型的,若base 是数组类型,则捕获器改为对应的数组类型
let traps: ProxyHandler<object | Array<any>> = objectTraps
if (isArray) {
target = [state] as any
traps = arrayTraps
}
//生产Proxy代理对象
const {revoke, proxy} = Proxy.revocable(target, traps)
state.draft_ = proxy as any
state.revoke_ = revoke
return proxy as any
}
processResult
// 解析结果,并返回copy数据 或者 base数据
export function processResult(result: any, scope: ImmerScope) {
scope.unfinalizedDrafts_ = scope.drafts_.length
//获取数据的代理对象
const baseDraft = scope.drafts_![0]
const isReplaced = result !== undefined && result !== baseDraft
if (!scope.immer_.useProxies_)
getPlugin("ES5").willFinalizeES5_(scope, result, isReplaced)
if (isReplaced) {
if (baseDraft[DRAFT_STATE].modified_) {
revokeScope(scope)
die(4)
}
if (isDraftable(result)) {
// Finalize the result in case it contains (or is) a subset of the draft.
result = finalize(scope, result)
if (!scope.parent_) maybeFreeze(scope, result)
}
if (scope.patches_) {
getPlugin("Patches").generateReplacementPatches_(
baseDraft[DRAFT_STATE],
result,
scope.patches_,
scope.inversePatches_!
)
}
} else {
// Finalize the base draft.
// result结果一般为undefined,所以一般为直接调用finalize,这个函数对返回state.copy_
result = finalize(scope, baseDraft, [])
}
revokeScope(scope)
if (scope.patches_) {
scope.patchListener_!(scope.patches_, scope.inversePatches_!)
}
return result !== NOTHING ? result : undefined
}
总结下
从源码大致可以看出,传入produce的数据value(value为被drafted,主要为引用类型),会通过proxy生成一个代理对象valueProxy和对应的state。
state是每个value对象对应的状态,用于记录数据是否发生变更,是否需要生成新的copy,是否需要进行替换等。
valueProxy可以理解为我们操作的draft,valueProxy通过监听get、set方法等,根据state来判断和处理数据。
最终返回我们处理后的不可变数据。
参考
1.ECMAScript 6 入门
2.immer官网文档
3.精读《Immer.js》源码——黄子毅
4.Immer 全解析——Sheepy