immer源码解析(未完待续)
MyPrototypeWhat opened this issue · 0 comments
MyPrototypeWhat commented
[TOC]
immer源码解析
入口
src/immer.ts
// src/immer.ts#L23
const immer = new Immer()
// src/immer.ts#L44
export const produce: IProduce = immer.produce
produce
例子
// produce(baseState, recipe: (draftState) => void): nextState
import produce from "immer"
const baseState = [
{
title: "Learn TypeScript",
done: true
},
{
title: "Try Immer",
done: false
}
]
const nextState = produce(baseState, draftState => {
draftState.push({title: "Tweet about it"})
draftState[1].done = true
})
produce
中对象的更改不会导致原值更改
expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)
// same for the changed 'done' prop
expect(baseState[1].done).toBe(false)
expect(nextState[1].done).toBe(true)
// unchanged data is structurally shared
expect(nextState[0]).toBe(baseState[0])
// ...but changed data isn't.
expect(nextState[1]).not.toBe(baseState[1])
源码
接下来看源码,看着不长,但是引用的函数较多
export class Immer implements ProducersFns {
......
/**
* @param {any} base - the initial state
* @param {Function} producer - function that receives a proxy of the base state as first argument and which can be freely modified
* @param {Function} patchListener - optional function that will be called with all the patches produced here
* @returns {any} a new state, or the initial state if nothing was modified
*/
produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
// curried invocation
// 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
}
}
// 错误信息 6: "The first or second argument to `produce` must be a function"
if (typeof recipe !== "function") die(6)
// 错误信息 7: "The third argument to `produce` must be a function or undefined"
if (patchListener !== undefined && typeof patchListener !== "function")
die(7)
let result
// 判断对象是否是是Draftable
if (isDraftable(base)) {
// 生成作用域,一个对象对应一个作用域
// this是Immer类的实例
const scope = enterScope(this)
// 添加proxy代理,这个是重点之一
const proxy = createProxy(this, base, undefined)
let hasError = true
try {
// 执行回调
result = recipe(proxy)
hasError = false
} finally {
// finally 而不是 catch + rethrow 更好地保留原始堆栈
// revokeScope:终止当前scope的所有proxy,将currentScope变量赋值为scope.parent_
if (hasError) revokeScope(scope)
else leaveScope(scope)
}
if (typeof Promise !== "undefined" && result instanceof 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 === undefined) result = base
if (result === NOTHING) result = undefined
if (this.autoFreeze_) freeze(result, true)
if (patchListener) {
const p: Patch[] = []
const ip: Patch[] = []
getPlugin("Patches").generateReplacementPatches_(base, result, p, ip)
patchListener(p, ip)
}
return result
} else die(21, base)
}
isDraftable
对类型的判断,没啥注释,一看就懂
export function isDraftable(value: any): boolean {
if (!value) return false
return (
isPlainObject(value) ||
Array.isArray(value) ||
!!value[DRAFTABLE] ||
!!value.constructor[DRAFTABLE] ||
isMap(value) ||
isSet(value)
)
}
-
isPlainObject
:const objectCtorString = Object.prototype.constructor.toString() export function isPlainObject(value: any): boolean { if (!value || typeof value !== "object") return false const proto = Object.getPrototypeOf(value) if (proto === null) { return true } const Ctor = Object.hasOwnProperty.call(proto, "constructor") && proto.constructor if (Ctor === Object) return true return ( typeof Ctor == "function" && // 字符串的比较主要是为了解决不同js环境,例如iframe Function.toString.call(Ctor) === objectCtorString ) }
-
DRAFTABLE
:export const DRAFTABLE: unique symbol = hasSymbol ? Symbol.for("immer-draftable") : ("__$immer_draftable" as any)
-
isMap\isSet
:// hasMap\hasSet 是否有map set的构造函数 export function isMap(target: any): target is AnyMap { return hasMap && target instanceof Map } export function isSet(target: any): target is AnySet { return hasSet && target instanceof Set }
enterScope
let currentScope: ImmerScope | undefined
export function enterScope(immer: Immer) {
return (currentScope = createScope(currentScope, immer))
}
function createScope(
parent_: ImmerScope | undefined,
immer_: Immer
): ImmerScope {
return {
// 保存着每次通过createProxy生成的proxy对象
// 例1
drafts_: [],
parent_,
immer_,
// 每当修改后的draft包含来自另一个范围的draft时,我们需要防止自动冻结,以便最终确定无主draft。
canAutoFreeze_: true,
unfinalizedDrafts_: 0
}
}
-
例1
const obj = {a: {c: 1}, b: 1} const produce = immer.produce const p2 = produce(a, draft => { draft.a.c = 3 }) // 此时 // scope.drafts_=>[Proxy,Proxy] // 数组第一项proxy是代理变量obj创建的,第二项是代理obj.a创建的
createProxy
重点之一,为数据添加proxy
src/core/proxy.ts#L50
公共参数和函数
// src/types/types-internal.ts#L20
export const enum Archtype {
Object,
Array,
Map,
Set
}
// src/types/types-internal.ts#L27
export const enum ProxyType {
ProxyObject,
ProxyArray,
Map,
Set,
ES5Object,
ES5Array
}
// src/utils/common.ts#L160
export function latest(state: ImmerState): any {
// 优先返回copy_,因为copy_保存的是base更改之后的拷贝
return state.copy_ || state.base_
}
// src/utils/common.ts#L118
export function has(thing: any, prop: PropertyKey): boolean {
return getArchtype(thing) === Archtype.Map
// 区分map
// 判断是否有这个属性
? thing.has(prop)
: Object.prototype.hasOwnProperty.call(thing, prop)
}
// src/utils/common.ts#L101
export function getArchtype(thing: any): Archtype {
/* istanbul ignore next */
// 如果只是一个plainObject,则state为undefined
const state: undefined | ImmerState = thing[DRAFT_STATE]
return state
? state.type_ > 3
? state.type_ - 4 // cause Object and Array map back from 4 and 5
: (state.type_ as any) // others are the same
: Array.isArray(thing)
? Archtype.Array
: isMap(thing)
? Archtype.Map
: isSet(thing)
? Archtype.Set
// 例如 如果原数据是对象,最终判断会落在这
: Archtype.Object
}
源码
export function createProxy<T extends Objectish>(
immer: Immer,
value: T,
parent?: ImmerState
): Drafted<T, ImmerState> {
// 前提条件:createProxy应该由isDraftable保护,所以我们知道我们可以安全地起草
// 判断对象的类型,使用不同的方法
const draft: Drafted = isMap(value)
? getPlugin("MapSet").proxyMap_(value, parent)
: isSet(value)
? getPlugin("MapSet").proxySet_(value, parent)
// 是否支持proxy
: immer.useProxies_
? createProxyProxy(value, parent)
: getPlugin("ES5").createES5Proxy_(value, parent)
const scope = parent ? parent.scope_ : getCurrentScope()
scope.drafts_.push(draft)
return draft
}
export function getCurrentScope() {
// 错误信息 0: "Illegal state"
if (__DEV__ && !currentScope) die(0)
return currentScope!
}
Object
-
createProxyProxy(value, parent)
export function createProxyProxy<T extends Objectish>( base: T, parent?: ImmerState ): Drafted<T, ProxyState> { const isArray = Array.isArray(base) const state: ProxyState = { // 类型判断 type_: isArray ? ProxyType.ProxyArray : (ProxyType.ProxyObject as any), // 跟踪与哪个produce调用相关联。 scope_: parent ? parent.scope_ : getCurrentScope()!, // 对浅层和深层变化都适用。 modified_: false, // 在最终确定期间使用。 finalized_: false, // 跟踪已赋值 (true) 或删除 (false) 的属性。 assigned_: {}, // The parent draft state. parent_: parent, // The base state. base_: base, // proxy代理对象 draft_: null as any, // set below // base更改值之后的副本 copy_: null, // 用来撤销proxy revoke_: null as any, isManual_: false } // 并不是以原数据作为proxy的target,而是使用state let target: T = state as any let traps: ProxyHandler<object | Array<any>> = objectTraps if (isArray) { target = [state] as any traps = arrayTraps } // Proxy.revocable()方法可以用来创建一个可撤销的代理对象。 const {revoke, proxy} = Proxy.revocable(target, traps) state.draft_ = proxy as any state.revoke_ = revoke return proxy as any } // src/core/proxy.ts#L241 function readPropFromProto(state: ImmerState, source: any, prop: PropertyKey) { // 获取对应属性描述符 const desc = getDescriptorFromProto(source, prop) // 返回对应值 return desc ? `value` in desc ? desc.value : // This is a very special case, if the prop is a getter defined by the // prototype, we should invoke it with the draft as context! desc.get?.call(state.draft_) : undefined } // src/core/proxy.ts#L252 // 遍历原型链,获取对应属性描述符 function getDescriptorFromProto( source: any, prop: PropertyKey ): PropertyDescriptor | undefined { // 'in' checks proto! if (!(prop in source)) return undefined let proto = Object.getPrototypeOf(source) while (proto) { const desc = Object.getOwnPropertyDescriptor(proto, prop) if (desc) return desc proto = Object.getPrototypeOf(proto) } return undefined }
-
Object:
objectTraps
export const objectTraps: ProxyHandler<ProxyState> = { get(state, prop) { if (prop === DRAFT_STATE) return state // 拿到原数据 const source = latest(state) if (!has(source, prop)) { // 判断是否有该属性 // 如果没有就往原型链上查 return readPropFromProto(state, source, prop) } // 获取值 const value = source[prop] if (state.finalized_ || !isDraftable(value)) { // state.finalized_后面讲 // 如果value不是一个可以被draft的对象,直接返回值 return value } // 比较value和base的引用 if (value === peek(state.base_, prop)) { // state.copy_浅拷贝state,base_ prepareCopy(state) // 为copy_中对应的props添加proxy return (state.copy_![prop as any] = createProxy( state.scope_.immer_, value, state )) } return value }, // 直接看代码吧,使用最新数据进行判断 // ------------ has(state, prop) { return prop in latest(state) }, ownKeys(state) { return Reflect.ownKeys(latest(state)) }, // ------------ set( state: ProxyObjectState, prop: string /* 严格来说不是,但有助于TS */, value ) { const desc = getDescriptorFromProto(latest(state), prop) if (desc?.set) { // 特殊情况:如果这个写入被 setter 捕获,我们必须用正确的上下文触发它 desc.set.call(state.draft_, value) return true } if (!state.modified_) { // 当前state是否经过修改 const current = peek(latest(state), prop) // 获取state const currentState: ProxyObjectState = current?.[DRAFT_STATE] // 特殊情况,如果将原值赋值给draft,可以直接忽略这个赋值 // 例1 if (currentState && currentState.base_ === value) { state.copy_![prop] = value state.assigned_[prop] = false return true } // Object.is // 值相等 && (value不为undefined 或者 base上有该属性) // 需要判断原数据没有prop 和 prop值为undefined的区别 // 如果原数据上没有prop,current值也会是undefined if (is(value, current) && (value !== undefined || has(state.base_, prop))) // 无需更改,可以忽略 return true // 拷贝 prepareCopy(state) // 修改当前state.modified_ 以及 parent.modified_ 为 true markChanged(state) } if ( // 排除设置值为自身引用的情况 // Fixes immerjs/immer#648 https://github.com/immerjs/immer/issues/648 state.copy_![prop] === value && // 排除0===-0 typeof value !== "number" && // 特殊情况 处理新的prop值为undefined情况 (value !== undefined || prop in state.copy_) ) return true // @ts-ignore state.copy_![prop] = value state.assigned_[prop] = true return true }, // delete 操作符的捕捉器 deleteProperty(state, prop: string) { // The `undefined` check is a fast path for pre-existing keys. if (peek(state.base_, prop) !== undefined || prop in state.base_) { state.assigned_[prop] = false prepareCopy(state) markChanged(state) } else { // if an originally not assigned property was deleted delete state.assigned_[prop] } // @ts-ignore if (state.copy_) delete state.copy_[prop] return true }, // Note: We never coerce `desc.value` into an Immer draft, because we can't make // the same guarantee in ES5 mode. // 可拦截 Object.getOwnPropertyDescriptor()、Reflect.getOwnPropertyDescriptor() getOwnPropertyDescriptor(state, prop) { const owner = latest(state) const desc = Reflect.getOwnPropertyDescriptor(owner, prop) if (!desc) return desc return { writable: true, configurable: state.type_ !== ProxyType.ProxyArray || prop !== "length", enumerable: desc.enumerable, value: owner[prop] } }, defineProperty() { die(11) }, getPrototypeOf(state) { return Object.getPrototypeOf(state.base_) }, setPrototypeOf() { die(12) } }
-
例1
const a = {a: {c: 1}, b: 2} const p1 = produce(a, draft => { draft.a = a.a })
-
-
Array:
Map
Set
processResult
src/core/finalize.ts#L20
重点,将更改之后的结果输出
export function processResult(result: any, scope: ImmerScope) {
scope.unfinalizedDrafts_ = scope.drafts_.length
// 基于base生成的proxy,会在scope.drafts_第一项
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].base_,
result,
scope.patches_,
scope.inversePatches_!
)
}
} else {
// 计算最终值
result = finalize(scope, baseDraft, [])
}
revokeScope(scope)
if (scope.patches_) {
scope.patchListener_!(scope.patches_, scope.inversePatches_!)
}
return result !== NOTHING ? result : undefined
}
finalize
src/core/finalize.ts#L57
function finalize(rootScope: ImmerScope, value: any, path?: PatchPath) {
// 不要在递归数据结构中递归
// 判断value是否冻结,兼容ie (Fixes #600)
if (isFrozen(value)) return value
const state: ImmerState = value[DRAFT_STATE]
// 一个普通的对象,可能需要冻结,可能包含draft
if (!state) {
// 遍历,根据格式判断分为两种:
// Object.keys().forEach \ .forEach
each(
value,
(key, childValue) =>
finalizeProperty(rootScope, state, value, key, childValue, path),
true // See #590, 不要递归到不可枚举的非draft对象
)
return value
}
// 永远不要最终确定另一个范围拥有的drafts
if (state.scope_ !== rootScope) return value
// 未修改的,返回(冻结的)原值
if (!state.modified_) {
maybeFreeze(rootScope, state.base_, true)
return state.base_
}
// 还没有最终确定,现在就开始吧
if (!state.finalized_) {
state.finalized_ = true
state.scope_.unfinalizedDrafts_--
const result =
// For ES5, create a good copy from the draft first, with added keys and without deleted keys.
state.type_ === ProxyType.ES5Object || state.type_ === ProxyType.ES5Array
? (state.copy_ = shallowCopy(state.draft_))
: state.copy_
// Finalize all children of the copy
// For sets we clone before iterating, otherwise we can get in endless loop due to modifying during iteration, see #628
// Although the original test case doesn't seem valid anyway, so if this in the way we can turn the next line
// back to each(result, ....)
each(
state.type_ === ProxyType.Set ? new Set(result) : result,
(key, childValue) =>
finalizeProperty(rootScope, state, result, key, childValue, path)
)
// everything inside is frozen, we can freeze here
maybeFreeze(rootScope, result, false)
// first time finalizing, let's create those patches
if (path && rootScope.patches_) {
getPlugin("Patches").generatePatches_(
state,
path,
rootScope.patches_,
rootScope.inversePatches_!
)
}
}
return state.copy_
}