Immer 源码分析——State 和 Scope 的设计原因
我将分析这个 Immer 仓库的代码,了解为什么存在 state 和 scope 这两个中间层,以及它们的设计原因。让我先查看相关的核心文件。
[5 tools called]
基于对 Immer 代码库的分析,我来详细解释为什么需要 state 和 scope 这两个中间层,以及它们各自的作用和设计原因。
State 的作用和设计原因
1. 状态跟踪和管理
state(在代码中表现为 ImmerState 及其子类型)主要负责跟踪每个 draft 对象的状态信息:
export interface ImmerBaseState {
parent_?: ImmerState
scope_: ImmerScope
modified_: boolean
finalized_: boolean
isManual_: boolean
}设计原因:
- 修改跟踪:
modified_标志记录对象是否被修改,避免不必要的复制 - 层级关系:
parent_维护父子 draft 关系,实现嵌套对象的修改传播 - 作用域关联:
scope_将 draft 与特定的produce调用关联
2. 复制机制优化
在 proxy.ts 中,state 管理着复制策略:
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 调用上下文:
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 对象:
export function revokeScope(scope: ImmerScope) {
leaveScope(scope)
scope.drafts_.forEach(revokeDraft)
// @ts-ignore
scope.drafts_ = null
}设计原因:
- 统一清理:确保所有相关资源在作用域结束时被正确清理
- 内存安全:防止内存泄漏,特别是在错误情况下
3. Patch 功能支持
scope 为 patch 功能提供基础设施:
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 的关联:
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 方法中完整的生命周期管理:
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)
}设计优势总结
- 关注点分离:state 关注单个对象状态,scope 关注调用上下文
- 性能优化:惰性复制、修改跟踪等机制大幅提升性能
- 错误恢复:作用域机制确保在错误情况下能正确清理资源
- 功能扩展:为 patch、嵌套调用等高级功能提供基础设施
- 内存安全:统一的资源管理防止内存泄漏
这种分层设计使得 Immer 能够高效处理复杂的不可变更新场景,同时保持代码的可维护性和扩展性。
[1 tool called]
基于对 Immer 代码库的分析,我来详细解释为什么需要 state 和 scope 这两个中间层,以及它们各自的设计原因:
State(状态)的作用和设计原因
1. 状态的核心作用
state 是每个 draft(草稿)对象内部的状态管理结构,它存储了 draft 的关键信息:
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 调用过程:
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. 创建时的关联
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 的最终化
- 统一生成补丁(如果启用)
- 统一应用自动冻结
设计优势总结
- 职责分离: state 管理单个 draft 的状态,scope 管理整个 produce 调用的上下文
- 性能优化: 只有在真正修改时才创建副本,避免不必要的内存分配
- 错误安全: 作用域机制确保在错误时能正确清理资源
- 功能扩展: 为补丁、撤销等高级功能提供了基础设施
- 配置管理: 通过 scope 继承 Immer 实例配置,保持行为一致性
这种分层设计使得 Immer 能够高效地处理复杂的不可变状态更新,同时提供了丰富的功能和良好的错误处理机制。
让我通过两个具体的 demo 来讲解 state 和 scope 的工作机制:
Demo 1: 基本对象修改
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 的工作流程:
创建 Scope:
enterScope(this)创建一个新的作用域- 当前 scope 记录 Immer 实例配置和 drafts 数组
创建 Drafts:
createProxy(baseState, undefined)创建根 draft- 每个 draft 内部都有
DRAFT_STATE属性指向其 state - state 包含
base_(原始对象),copy_(null),modified_(false)
修改过程:
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_
- 访问
最终化:
processResult(result, scope)处理所有 drafts- 只对修改过的对象创建新副本,未修改的保持引用
- 返回最终的新状态
Demo 2: 错误处理和补丁生成
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 的错误处理:
Scope 的错误恢复:
95:107:src/core/immerClass.tslet hasError = true try { result = recipe(proxy) hasError = false } finally { if (hasError) revokeScope(scope) else leaveScope(scope) }错误发生时的处理:
hasError保持为 truefinally块中调用revokeScope(scope)revokeScope会撤销所有已创建的 drafts- 保持原子性:要么全部成功,要么全部回滚
补丁机制:
- 如果启用补丁监听器,scope 会存储
patches_和inversePatches_ - 每个修改操作都会生成相应的补丁
- 最终统一传递给监听器
- 如果启用补丁监听器,scope 会存储
关键设计优势体现
1. 性能优化(Copy-on-Write)
export function prepareCopy(state: {
base_: any
copy_: any
scope_: ImmerScope
}) {
if (!state.copy_) {
state.copy_ = shallowCopy(
state.base_,
state.scope_.immer_.useStrictShallowCopy_
)
}
}只有在第一次修改时才创建副本,避免不必要的内存分配。
2. 修改传播机制
export function markChanged(state: ImmerState) {
if (!state.modified_) {
state.modified_ = true
if (state.parent_) {
markChanged(state.parent_)
}
}
}子对象的修改会自动传播到所有父级,确保正确的修改跟踪。
3. 类型安全的代理处理
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
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 属性
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 属性时:
- 只修改
userdraft 的copy_中的age属性 - 不会重新创建整个
user对象
3. 最终化过程
if (!state.modified_) {
maybeFreeze(rootScope, state.base_, true)
return state.base_
}在 finalize 阶段:
- 如果
user对象没有被修改(modified_为 false),直接返回原始user对象 - 如果被修改了,返回
state.copy_(修改后的副本)
关键设计:结构共享
Immer 使用结构共享(Structural Sharing) 技术:
// 原始状态
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. 初始状态
const baseState = {
user: { name: "John", age: 30 },
todos: ["task1", "task2"]
};所有 draft 的初始状态:
modified_ = falsefinalized_ = falsecopy_ = null
2. 执行 draft.user.age = 31
步骤 1: 访问 draft.user
if (value === peek(state.base_, prop)) {
prepareCopy(state)
return (state.copy_![prop as any] = createProxy(value, state))
}- 创建
user对象的 draft(新的 state) userstate:modified_ = false,finalized_ = false,copy_ = null
步骤 2: 设置 age 属性
set(state, prop, value) {
prepareCopy(state)
markChanged(state)
state.copy_![prop] = value
state.assigned_[prop] = true
}prepareCopy(state):为userstate 创建copy_(浅拷贝)markChanged(state):标记userstate 为modified_ = true- 递归标记根 state 为
modified_ = true
3. 进入 processResult(result, scope)
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 函数的详细过程
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 处理子属性
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:
最终化根对象:
- 调用
finalize(scope, baseDraft, []) - 发现根 state
modified_ = true(因为 user 被修改) - 设置
finalized_ = true - 开始处理
copy_中的属性
- 调用
处理
user属性:user是一个 draft(有DRAFT_STATE)- 递归调用
finalize(rootScope, childValue, path)处理 user - user state
modified_ = true(age 被修改) - 设置 user state
finalized_ = true - 返回 user 的
copy_(包含 age=31)
处理
age属性:age是基本类型(31),不是 draft- 直接保留在 user 的
copy_中
处理其他属性:
name属性没有被修改,保持原始值 "John"todos数组没有被访问,保持原始引用
最终状态变化
修改前:
user state: { modified_: false, finalized_: false, copy_: null }修改后:
user state: { modified_: true, finalized_: true, copy_: { name: "John", age: 31 } }根 state:
root state: { modified_: true, finalized_: true, copy_: { user: {name: "John", age: 31}, todos: ["task1", "task2"] } }finalized_ 的关键作用
- 防止重复最终化:确保每个 draft 只被最终化一次
- 线程安全:在多线程环境中防止竞态条件(虽然 JS 是单线程)
- 状态机管理:明确标识 draft 的生命周期阶段
- 错误恢复:如果最终化过程中出错,可以知道哪些已经处理过
这就是为什么 finalized_ 标志位如此重要 - 它确保了最终化过程的正确性和幂等性。
[1 tool called]
当执行 draft.user.age = {old: '35'} 时,处理过程与设置基本类型值有所不同。让我详细讲解这个操作的具体流程:
操作流程详解
1. 初始状态
const baseState = {
user: { name: "John", age: 30 },
todos: ["task1", "task2"]
};2. 执行 draft.user.age = {old: '35'}
步骤 1: 访问 draft.user
if (value === peek(state.base_, prop)) {
prepareCopy(state)
return (state.copy_![prop as any] = createProxy(value, state))
}- 创建
user对象的 draft userstate:modified_ = false,finalized_ = false,copy_ = null
步骤 2: 设置 age 属性为对象
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
对于对象值:
get(state, prop) {
const value = source[prop]
if (state.finalized_ || !isDraftable(value)) {
return value
}
// ... 创建 draft 的逻辑
}当设置 age = {old: '35'} 时:
isDraftable({old: '35'})返回true(因为是普通对象)- 但是此时是在
set操作中,不是get操作 - 直接将新对象赋值给
copy_.age
4. 最终化过程中的特殊处理
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 = 31 | draft.user.age = {old: '35'} |
|---|---|---|
| 修改类型 | 修改现有属性值 | 替换整个属性值为新对象 |
| 内存影响 | 只修改 age 值 | 创建全新的 age 对象 |
| 结构共享 | 共享 user 对象的其他属性 | 完全中断 age 属性的共享 |
| 最终化处理 | 简单值直接赋值 | 需要递归处理新对象 |
性能考虑
这种设计意味着:
- ✅ 小对象赋值:性能影响很小
- ⚠️ 大对象赋值:可能产生较大的内存开销(因为创建全新对象)
- ✅ 结构感知:Immer 能智能处理各种赋值情况
这就是为什么在性能敏感的场景中,建议避免频繁赋值大对象,而是采用逐步修改现有对象的方式。