MyPrototypeWhat/take-down

immer源码解析(未完待续)

MyPrototypeWhat opened this issue · 0 comments

[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_
}