Oasis's Cloud

一个人的首要责任,就是要有雄心。雄心是一种高尚的激情,它可以采取多种合理的形式。
—— 《一个数学家的辩白》

Immer 源码分析——State 和 Scope 的设计原因

作者:oasis


我将分析这个 Immer 仓库的代码,了解为什么存在 state 和 scope 这两个中间层,以及它们的设计原因。让我先查看相关的核心文件。

[5 tools called]

基于对 Immer 代码库的分析,我来详细解释为什么需要 statescope 这两个中间层,以及它们各自的作用和设计原因。

State 的作用和设计原因

1. 状态跟踪和管理

state(在代码中表现为 ImmerState 及其子类型)主要负责跟踪每个 draft 对象的状态信息:

typescript
export interface ImmerBaseState {
	parent_?: ImmerState
	scope_: ImmerScope
	modified_: boolean
	finalized_: boolean
	isManual_: boolean
}

设计原因:

  • 修改跟踪modified_ 标志记录对象是否被修改,避免不必要的复制
  • 层级关系parent_ 维护父子 draft 关系,实现嵌套对象的修改传播
  • 作用域关联scope_ 将 draft 与特定的 produce 调用关联

2. 复制机制优化

proxy.ts 中,state 管理着复制策略:

typescript
const state: ProxyState = {
	type_: isArray ? ArchType.Array : (ArchType.Object 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
}

设计原因:

  • 惰性复制copy_ 只在真正修改时才创建,节省内存和性能
  • 赋值跟踪assigned_ 记录哪些属性被赋值,优化最终化过程
  • 类型区分type_ 区分对象和数组,应用不同的代理策略

Scope 的作用和设计原因

1. 调用上下文管理

scope 代表一个完整的 produce 调用上下文:

typescript
export interface ImmerScope {
	patches_?: Patch[]
	inversePatches_?: Patch[]
	canAutoFreeze_: boolean
	drafts_: any[]
	parent_?: ImmerScope
	patchListener_?: PatchListener
	immer_: Immer
	unfinalizedDrafts_: number
}

设计原因:

  • 调用隔离:每个 produce 调用有独立的作用域,避免状态污染
  • 错误恢复:作用域可以在出错时被撤销(revokeScope
  • 嵌套支持parent_ 支持嵌套的 produce 调用

2. 资源管理和清理

scope 负责管理所有相关的 draft 对象:

typescript
export function revokeScope(scope: ImmerScope) {
	leaveScope(scope)
	scope.drafts_.forEach(revokeDraft)
	// @ts-ignore
	scope.drafts_ = null
}

设计原因:

  • 统一清理:确保所有相关资源在作用域结束时被正确清理
  • 内存安全:防止内存泄漏,特别是在错误情况下

3. Patch 功能支持

scope 为 patch 功能提供基础设施:

typescript
export function usePatchesInScope(
	scope: ImmerScope,
	patchListener?: PatchListener
) {
	if (patchListener) {
		getPlugin("Patches") // assert we have the plugin
		scope.patches_ = []
		scope.inversePatches_ = []
		scope.patchListener_ = patchListener
	}
}

State 和 Scope 的协同工作机制

1. 创建关联

createProxy 中建立 state 和 scope 的关联:

typescript
export function createProxy<T extends Objectish>(
	value: T,
	parent?: ImmerState
): Drafted<T, ImmerState> {
	// precondition: createProxy should be guarded by isDraftable, so we know we can safely draft
	const draft: Drafted = isMap(value)
		? getPlugin("MapSet").proxyMap_(value, parent)
		: isSet(value)
		? getPlugin("MapSet").proxySet_(value, parent)
		: createProxyProxy(value, parent)

	const scope = parent ? parent.scope_ : getCurrentScope()
	scope.drafts_.push(draft)
	return draft
}

2. 生命周期管理

produce 方法中完整的生命周期管理:

typescript
const scope = enterScope(this)
const proxy = createProxy(base, undefined)
let hasError = true
try {
	result = recipe(proxy)
	hasError = false
} finally {
	// finally instead of catch + rethrow better preserves original stack
	if (hasError) revokeScope(scope)
	else leaveScope(scope)
}

设计优势总结

  1. 关注点分离:state 关注单个对象状态,scope 关注调用上下文
  2. 性能优化:惰性复制、修改跟踪等机制大幅提升性能
  3. 错误恢复:作用域机制确保在错误情况下能正确清理资源
  4. 功能扩展:为 patch、嵌套调用等高级功能提供基础设施
  5. 内存安全:统一的资源管理防止内存泄漏

这种分层设计使得 Immer 能够高效处理复杂的不可变更新场景,同时保持代码的可维护性和扩展性。

[1 tool called]

基于对 Immer 代码库的分析,我来详细解释为什么需要 statescope 这两个中间层,以及它们各自的设计原因:

State(状态)的作用和设计原因

1. 状态的核心作用

state 是每个 draft(草稿)对象内部的状态管理结构,它存储了 draft 的关键信息:

txt
export interface ImmerBaseState {
	parent_?: ImmerState
	scope_: ImmerScope
	modified_: boolean
	finalized_: boolean
	isManual_: boolean
}

2. 状态的设计原因

a. 跟踪修改状态

  • modified_: 标记该 draft 是否被修改过
  • assigned_: 跟踪哪些属性被赋值或删除(在 proxy.ts 中定义)

b. 管理复制机制

  • base_: 存储原始状态
  • copy_: 存储修改后的副本
  • 只有在真正修改时才创建副本(copy-on-write)

c. 维护父子关系

  • parent_: 指向父级 state,用于级联标记修改
  • 当子 draft 被修改时,会自动标记所有父级为已修改

d. 类型区分

  • type_: 区分对象、数组、Map、Set 等不同类型
  • 不同类型的对象需要不同的代理处理策略

Scope(作用域)的作用和设计原因

1. 作用域的核心作用

scope 代表一个完整的 produce 调用过程:

txt
export interface ImmerScope {
	patches_?: Patch[]
	inversePatches_?: Patch[]
	canAutoFreeze_: boolean
	drafts_: any[]
	parent_?: ImmerScope
	patchListener_?: PatchListener
	immer_: Immer
	unfinalizedDrafts_: number
}

2. 作用域的设计原因

a. 生命周期管理

  • 每个 produce 调用创建一个新的 scope
  • 通过 enterScope/leaveScope 管理调用栈

b. 批量处理 drafts

  • drafts_: 存储该作用域内创建的所有 draft
  • 在最终化时统一处理所有相关 drafts

c. 补丁生成

  • patches_inversePatches_: 存储变更补丁
  • 支持撤销/重做功能

d. 配置继承

  • immer_: 引用 Immer 实例,继承配置(如 autoFreeze 设置)
  • 确保同一 produce 调用内的行为一致性

e. 错误恢复

  • 如果 recipe 函数抛出错误,可以 revokeScope 撤销所有修改
  • 保持原子性操作

State 和 Scope 的协同工作机制

1. 创建时的关联

txt
const scope = parent ? parent.scope_ : getCurrentScope()
scope.drafts_.push(draft)
return draft

每个 draft 的 state 都引用其所属的 scope,而 scope 则记录所有属于它的 drafts。

2. 修改传播机制

当 draft 被修改时:

  • 首先标记自己的 modified_ = true
  • 然后递归向上标记所有父级 state 为已修改
  • 最终 scope 在最终化时收集所有修改

3. 最终化处理

processResult 中:

  • Scope 负责协调所有 drafts 的最终化
  • 统一生成补丁(如果启用)
  • 统一应用自动冻结

设计优势总结

  1. 职责分离: state 管理单个 draft 的状态,scope 管理整个 produce 调用的上下文
  2. 性能优化: 只有在真正修改时才创建副本,避免不必要的内存分配
  3. 错误安全: 作用域机制确保在错误时能正确清理资源
  4. 功能扩展: 为补丁、撤销等高级功能提供了基础设施
  5. 配置管理: 通过 scope 继承 Immer 实例配置,保持行为一致性

这种分层设计使得 Immer 能够高效地处理复杂的不可变状态更新,同时提供了丰富的功能和良好的错误处理机制。

让我通过两个具体的 demo 来讲解 state 和 scope 的工作机制:

Demo 1: 基本对象修改

javascript
const baseState = {
  user: {
    name: "John",
    age: 30,
    address: {
      city: "New York",
      country: "USA"
    }
  },
  todos: ["task1", "task2"]
};

const nextState = produce(baseState, draft => {
  draft.user.age = 31;
  draft.todos.push("task3");
});

State 和 Scope 的工作流程:

  1. 创建 Scope:

    • enterScope(this) 创建一个新的作用域
    • 当前 scope 记录 Immer 实例配置和 drafts 数组
  2. 创建 Drafts:

    • createProxy(baseState, undefined) 创建根 draft
    • 每个 draft 内部都有 DRAFT_STATE 属性指向其 state
    • state 包含 base_(原始对象), copy_(null), modified_(false)
  3. 修改过程:

    • draft.user.age = 31:

      • 访问 draft.user → 创建 user 对象的 draft (新的 state)
      • 设置 age 属性 → 标记 user state 为 modified_ = true
      • 递归标记根 state 为 modified_ = true
      • 创建 user 对象的 copy_ (浅拷贝)
    • draft.todos.push("task3"):

      • 访问 draft.todos → 创建数组的 draft
      • push 操作 → 标记数组 state 为 modified_ = true
      • 递归标记根 state 为 modified_ = true
      • 创建数组的 copy_
  4. 最终化:

    • processResult(result, scope) 处理所有 drafts
    • 只对修改过的对象创建新副本,未修改的保持引用
    • 返回最终的新状态

Demo 2: 错误处理和补丁生成

javascript
const baseState = { count: 0, items: [] };

try {
  const nextState = produce(baseState, draft => {
    draft.count = 1;
    throw new Error("Something went wrong");
    draft.items.push("item1"); // 这行不会执行
  }, (patches, inversePatches) => {
    console.log("Patches:", patches);
    console.log("Inverse patches:", inversePatches);
  });
} catch (error) {
  console.log("Error caught:", error.message);
}

State 和 Scope 的错误处理:

  1. Scope 的错误恢复:

    95:107:src/core/immerClass.ts
    let hasError = true
    try {
      result = recipe(proxy)
      hasError = false
    } finally {
      if (hasError) revokeScope(scope)
      else leaveScope(scope)
    }
  2. 错误发生时的处理:

    • hasError 保持为 true
    • finally 块中调用 revokeScope(scope)
    • revokeScope 会撤销所有已创建的 drafts
    • 保持原子性:要么全部成功,要么全部回滚
  3. 补丁机制:

    • 如果启用补丁监听器,scope 会存储 patches_inversePatches_
    • 每个修改操作都会生成相应的补丁
    • 最终统一传递给监听器

关键设计优势体现

1. 性能优化(Copy-on-Write)

txt
export function prepareCopy(state: {
	base_: any
	copy_: any
	scope_: ImmerScope
}) {
	if (!state.copy_) {
		state.copy_ = shallowCopy(
			state.base_,
			state.scope_.immer_.useStrictShallowCopy_
		)
	}
}

只有在第一次修改时才创建副本,避免不必要的内存分配。

2. 修改传播机制

txt
export function markChanged(state: ImmerState) {
	if (!state.modified_) {
		state.modified_ = true
		if (state.parent_) {
			markChanged(state.parent_)
		}
	}
}

子对象的修改会自动传播到所有父级,确保正确的修改跟踪。

3. 类型安全的代理处理

txt
export const objectTraps: ProxyHandler<ProxyState> = {
	get(state, prop) {
		if (prop === DRAFT_STATE) return state
		// ... 复杂的属性访问逻辑
	},
	set(state, prop, value) {
		// ... 复杂的属性设置逻辑
	}
}

不同的对象类型(普通对象、数组、Map、Set)有不同的代理陷阱,确保类型安全的行为。

这种分层设计使得 Immer 能够:

  • ✅ 高效处理深层嵌套对象的修改
  • ✅ 提供原子性操作(全成功或全回滚)
  • ✅ 支持高级功能如补丁生成
  • ✅ 保持优秀的性能表现

[1 tool called]

不会! draft.user.age = 31 不会让整个 user 对象变为全新的对象。让我详细解释这个过程:

具体执行流程

1. 访问 draft.user

txt
if (value === peek(state.base_, prop)) {
	prepareCopy(state)
	return (state.copy_![prop as any] = createProxy(value, state))
}

当访问 draft.user 时:

  • 检查 user 属性是否等于原始值
  • 如果相等,调用 prepareCopy(state) 准备副本
  • 创建 user 对象的 draft 并返回

2. 设置 age 属性

txt
set(
	state: ProxyObjectState,
	prop: string /* strictly not, but helps TS */,
	value
) {
	// ... 验证和准备逻辑
	prepareCopy(state)
	markChanged(state)
	// @ts-ignore
	state.copy_![prop] = value
	state.assigned_[prop] = true
	return true
}

设置 age 属性时:

  • 只修改 user draft 的 copy_ 中的 age 属性
  • 不会重新创建整个 user 对象

3. 最终化过程

txt
if (!state.modified_) {
	maybeFreeze(rootScope, state.base_, true)
	return state.base_
}

finalize 阶段:

  • 如果 user 对象没有被修改(modified_ 为 false),直接返回原始 user 对象
  • 如果被修改了,返回 state.copy_(修改后的副本)

关键设计:结构共享

Immer 使用结构共享(Structural Sharing) 技术:

javascript
// 原始状态
const baseState = {
  user: { name: "John", age: 30, address: { city: "NY", country: "USA" } },
  todos: ["task1", "task2"]
};

// 修改后
const nextState = produce(baseState, draft => {
  draft.user.age = 31;
});

// 结果:
console.log(baseState.user === nextState.user); // false (user 对象不同)
console.log(baseState.user.address === nextState.user.address); // true (address 对象相同)
console.log(baseState.todos === nextState.todos); // true (todos 数组相同)

内存结构示意图:

baseState: {
  user: { name: "John", age: 30, address: {...} },
  todos: ["task1", "task2"]
}

nextState: {
  user: { name: "John", age: 31, address: {...} },  // 新的 user 对象
  todos: ["task1", "task2"]  // 相同的 todos 引用
}

只有真正被修改的部分才会创建新对象,未修改的部分保持引用共享。这就是为什么 Immer 既保证了不可变性,又保持了良好的性能。

让我详细讲解 draft.user.age = 31 操作中 finalized_ 的完整过程:

操作流程分解

1. 初始状态

javascript
const baseState = {
  user: { name: "John", age: 30 },
  todos: ["task1", "task2"]
};

所有 draft 的初始状态:

  • modified_ = false
  • finalized_ = false
  • copy_ = null

2. 执行 draft.user.age = 31

步骤 1: 访问 draft.user

txt
if (value === peek(state.base_, prop)) {
	prepareCopy(state)
	return (state.copy_![prop as any] = createProxy(value, state))
}
  • 创建 user 对象的 draft(新的 state)
  • user state: modified_ = false, finalized_ = false, copy_ = null

步骤 2: 设置 age 属性

txt
set(state, prop, value) {
	prepareCopy(state)
	markChanged(state)
	state.copy_![prop] = value
	state.assigned_[prop] = true
}
  • prepareCopy(state):为 user state 创建 copy_(浅拷贝)
  • markChanged(state):标记 user state 为 modified_ = true
  • 递归标记根 state 为 modified_ = true

3. 进入 processResult(result, scope)

txt
export function processResult(result: any, scope: ImmerScope) {
	scope.unfinalizedDrafts_ = scope.drafts_.length
	const baseDraft = scope.drafts_![0]
	const isReplaced = result !== undefined && result !== baseDraft
	if (isReplaced) {
		// ... 处理替换情况
	} else {
		// Finalize the base draft.
		result = finalize(scope, baseDraft, [])
	}
	revokeScope(scope)
	// ... 处理补丁
	return result !== NOTHING ? result : undefined
}

4. finalize 函数的详细过程

txt
function finalize(rootScope: ImmerScope, value: any, path?: PatchPath) {
	const state: ImmerState = value[DRAFT_STATE]
	
	// 未修改的 draft,返回原始对象
	if (!state.modified_) {
		maybeFreeze(rootScope, state.base_, true)
		return state.base_
	}
	
	// 首次最终化
	if (!state.finalized_) {
		state.finalized_ = true  // ← 这里设置 finalized_ = true
		state.scope_.unfinalizedDrafts_--
		const result = state.copy_
		
		// 最终化所有子属性
		each(resultEach, (key, childValue) =>
			finalizeProperty(rootScope, state, result, key, childValue, path, isSet)
		)
		
		// 冻结结果
		maybeFreeze(rootScope, result, false)
		
		// 生成补丁
		if (path && rootScope.patches_) {
			getPlugin("Patches").generatePatches_(state, path, rootScope.patches_, rootScope.inversePatches_!)
		}
	}
	return state.copy_
}

5. finalizeProperty 处理子属性

txt
function finalizeProperty(rootScope, parentState, targetObject, prop, childValue, rootPath, targetIsSet) {
	if (isDraft(childValue)) {
		const res = finalize(rootScope, childValue, path)
		set(targetObject, prop, res)
		// ... 其他逻辑
	}
	// ... 处理非 draft 对象
}

具体执行顺序

对于 draft.user.age = 31

  1. 最终化根对象

    • 调用 finalize(scope, baseDraft, [])
    • 发现根 state modified_ = true(因为 user 被修改)
    • 设置 finalized_ = true
    • 开始处理 copy_ 中的属性
  2. 处理 user 属性

    • user 是一个 draft(有 DRAFT_STATE
    • 递归调用 finalize(rootScope, childValue, path) 处理 user
    • user state modified_ = true(age 被修改)
    • 设置 user state finalized_ = true
    • 返回 user 的 copy_(包含 age=31)
  3. 处理 age 属性

    • age 是基本类型(31),不是 draft
    • 直接保留在 user 的 copy_
  4. 处理其他属性

    • name 属性没有被修改,保持原始值 "John"
    • todos 数组没有被访问,保持原始引用

最终状态变化

修改前

javascript
user state: { modified_: false, finalized_: false, copy_: null }

修改后

javascript
user state: { modified_: true, finalized_: true, copy_: { name: "John", age: 31 } }

根 state

javascript
root state: { modified_: true, finalized_: true, copy_: { user: {name: "John", age: 31}, todos: ["task1", "task2"] } }

finalized_ 的关键作用

  1. 防止重复最终化:确保每个 draft 只被最终化一次
  2. 线程安全:在多线程环境中防止竞态条件(虽然 JS 是单线程)
  3. 状态机管理:明确标识 draft 的生命周期阶段
  4. 错误恢复:如果最终化过程中出错,可以知道哪些已经处理过

这就是为什么 finalized_ 标志位如此重要 - 它确保了最终化过程的正确性和幂等性。

[1 tool called]

当执行 draft.user.age = {old: '35'} 时,处理过程与设置基本类型值有所不同。让我详细讲解这个操作的具体流程:

操作流程详解

1. 初始状态

javascript
const baseState = {
  user: { name: "John", age: 30 },
  todos: ["task1", "task2"]
};

2. 执行 draft.user.age = {old: '35'}

步骤 1: 访问 draft.user

txt
if (value === peek(state.base_, prop)) {
	prepareCopy(state)
	return (state.copy_![prop as any] = createProxy(value, state))
}
  • 创建 user 对象的 draft
  • user state: modified_ = false, finalized_ = false, copy_ = null

步骤 2: 设置 age 属性为对象

txt
set(state, prop, value) {
	// ... 验证逻辑
	prepareCopy(state)
	markChanged(state)
	// @ts-ignore
	state.copy_![prop] = value
	state.assigned_[prop] = true
	return true
}

这里的关键区别在于:{old: '35'} 是一个新对象,不是 draft

3. 特殊处理:对象赋值 vs 基本类型赋值

对于基本类型值(如数字、字符串)

  • 直接赋值到 copy_
  • 不需要创建新的 draft

对于对象值

txt
get(state, prop) {
	const value = source[prop]
	if (state.finalized_ || !isDraftable(value)) {
		return value
	}
	// ... 创建 draft 的逻辑
}

当设置 age = {old: '35'} 时:

  1. isDraftable({old: '35'}) 返回 true(因为是普通对象)
  2. 但是此时是在 set 操作中,不是 get 操作
  3. 直接将新对象赋值给 copy_.age

4. 最终化过程中的特殊处理

txt
if (isDraftable(childValue) && !childIsFrozen) {
	if (!rootScope.immer_.autoFreeze_ && rootScope.unfinalizedDrafts_ < 1) {
		// optimization: if an object is not a draft, and we don't have to
		// deepfreeze everything, and we are sure that no drafts are left in the remaining object
		// cause we saw and finalized all drafts already; we can stop visiting the rest of the tree.
		// This benefits especially adding large data tree's without further processing.
		// See add-data.js perf test
		return
	}
	// ... 其他检查
	finalize(rootScope, childValue)
	// ... 冻结逻辑
}

finalizeProperty 中:

  • 发现 age 属性是一个普通对象(不是 draft)
  • 但是 isDraftable({old: '35'}) 返回 true
  • 会递归调用 finalize(rootScope, childValue) 来处理这个新对象

具体内存变化

修改前:

baseState: {
  user: { name: "John", age: 30 }  // ← 原始对象
}

修改后:

nextState: {
  user: { 
    name: "John",           // ← 保持原始引用(未修改)
    age: { old: "35" }      // ← 全新的对象!
  }
}

draft.user.age = 31 的关键区别

操作draft.user.age = 31draft.user.age = {old: '35'}
修改类型修改现有属性值替换整个属性值为新对象
内存影响只修改 age创建全新的 age 对象
结构共享共享 user 对象的其他属性完全中断 age 属性的共享
最终化处理简单值直接赋值需要递归处理新对象

性能考虑

这种设计意味着:

  • 小对象赋值:性能影响很小
  • ⚠️ 大对象赋值:可能产生较大的内存开销(因为创建全新对象)
  • 结构感知:Immer 能智能处理各种赋值情况

这就是为什么在性能敏感的场景中,建议避免频繁赋值大对象,而是采用逐步修改现有对象的方式。