Immutable不可变数据原理深度解析
深入解析不可变数据的概念、结构共享机制、Immer.js原理及React/Vue中的应用
Immutable不可变数据原理深度解析
一句话概括
不可变数据是一种创建数据后永不修改,变更时返回新引用的设计模式,通过结构共享实现高效内存使用,是React/Vue性能优化的核心基础。
背景
在现代前端开发中,不可变数据是一个高频出现却又容易被忽视的概念:
- React的状态更新:为什么不能直接修改state?为什么要用
setState(newState)? - Vue的响应式系统:为什么Vue3用Proxy替代了Object.defineProperty?
- Redux的设计哲学:为什么reducer必须是纯函数,不能修改原状态?
这些问题的答案都指向同一个概念:不可变数据(Immutable Data)。
面试中,不可变数据是框架原理考察的必考点:
- “为什么React中不能直接修改state?”
- “Immer.js是怎么实现高效不可变更新的?”
- “什么是结构共享?它解决了什么问题?”
理解不可变数据,才能真正理解现代前端框架的性能优化原理。
概念与定义
什么是不可变数据?
不可变数据(Immutable Data):一旦创建,就不能被修改的数据。任何”修改”操作都会返回一个全新的数据结构,原数据保持不变。
1
2
3
4
5
6
7
8
// 可变数据(Mutable):直接修改原数据
const mutable = { name: 'Alice', age: 25 };
mutable.age = 26; // 直接修改,原数据变了
// 不可变数据(Immutable):返回新数据
const original = { name: 'Alice', age: 25 };
const updated = { ...original, age: 26 }; // 创建新对象
// original 仍然是 { name: 'Alice', age: 25 }
为什么需要不可变数据?
1. 引用相等判断
React的shouldComponentUpdate、React.memo、useMemo都依赖引用相等判断:
1
2
3
4
5
6
7
// ❌ 直接修改:引用不变,React检测不到变化
const [user, setUser] = useState({ name: 'Alice' });
user.name = 'Bob';
setUser(user); // 同一个引用,组件不会重新渲染
// ✅ 不可变更新:新引用,React能检测到变化
setUser({ ...user, name: 'Bob' }); // 新引用,触发重新渲染
2. 时间旅行调试
Redux DevTools的时间旅行功能依赖状态快照,如果直接修改原状态,就无法回溯。
3. 并发安全
多线程/多任务环境下,不可变数据不需要锁,天然线程安全。
不可变数据的代价
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 深层嵌套对象的更新非常繁琐
const state = {
user: {
profile: {
name: 'Alice',
address: {
city: 'Beijing'
}
}
}
};
// 嵌套展开...噩梦
const newState = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
address: {
...state.user.profile.address,
city: 'Shanghai'
}
}
}
};
这就是Immer.js要解决的问题。
最小示例
手动不可变更新
1
2
3
4
5
6
7
8
9
10
11
// 数组不可变操作
const arr = [1, 2, 3];
const newArr = [...arr, 4]; // 添加
const filtered = arr.filter(x => x !== 2); // 删除
const mapped = arr.map(x => x * 2); // 修改
// 对象不可变操作
const obj = { a: 1, b: 2 };
const newObj = { ...obj, c: 3 }; // 添加属性
const { a, ...rest } = obj; // 删除属性(ES2020)
const updated = { ...obj, a: 10 }; // 修改属性
使用Immer简化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { produce } from 'immer';
const state = {
user: {
profile: {
name: 'Alice',
address: { city: 'Beijing' }
}
}
};
// ✅ 使用Immer:写法像直接修改,实际是不可变更新
const newState = produce(state, draft => {
draft.user.profile.address.city = 'Shanghai';
});
// 原状态保持不变
console.log(state.user.profile.address.city); // 'Beijing'
console.log(newState.user.profile.address.city); // 'Shanghai'
核心知识点拆解
1. 不可变数据的实现方式
方式一:原生JavaScript(浅拷贝)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Object.assign(第一层是浅拷贝)
const original = { a: 1, b: { c: 2 } };
const copied = Object.assign({}, original, { a: 10 });
// copied.b 和 original.b 指向同一个对象!
// 展开运算符(同上)
const spread = { ...original, a: 10 };
// spread.b === original.b → true(浅拷贝)
// 数组方法(返回新数组)
const arr = [1, 2, 3];
arr.push(4); // ❌ 原地修改
arr.concat(4); // ✅ 返回新数组
[...arr, 4]; // ✅ 展开运算符
方式二:深拷贝(JSON + 递归)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// JSON方法(有局限)
const deepCopy = JSON.parse(JSON.stringify(obj));
// 问题:无法处理函数、Symbol、循环引用、Date、RegExp等
// 递归深拷贝(支持更多类型)
function deepClone(obj, cache = new Map()) {
if (obj === null || typeof obj !== 'object') return obj;
if (cache.has(obj)) return cache.get(obj);
const clone = Array.isArray(obj) ? [] : {};
cache.set(obj, clone);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], cache);
}
}
return clone;
}
问题:每次更新都深拷贝整个对象,性能太差。
方式三:结构共享(Persistent Data Structure)
这是Immer.js和Immutable.js的核心技术。
2. 结构共享机制(Structural Sharing)
核心思想:只复制”变化路径”上的节点,共享未变化的部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const original = {
a: { x: 1, y: 2 },
b: { x: 3, y: 4 },
c: 5
};
// 修改 a.x = 10
const updated = {
a: { x: 10, y: 2 }, // 新的 a 对象
b: original.b, // 复用原 b(共享)
c: original.c // 复用原 c(共享)
};
// 内存对比
original.a === updated.a; // false(a被复制)
original.b === updated.b; // true(b被共享)
original.c === updated.c; // true(c被共享)
可视化结构共享:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
原始数据结构:
root
/ | \
a b c
/| /| \
x y x y 5
1 2 3 4
修改 a.x = 10 后:
root' (新)
/ | \
a' b c ← b和c被共享(同一引用)
/|
x y
10 2
性能优势:
- 空间复杂度:O(变化路径深度),而非O(总数据量)
- 时间复杂度:只复制必要节点,效率高
3. Immer.js原理深度解析
Immer是”不可变数据”与”便捷写法”的完美结合。
核心概念:Draft(草稿)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { produce } from 'immer';
const state = { count: 0, list: [1, 2, 3] };
const nextState = produce(state, draft => {
// draft 是 state 的"代理"
// 对 draft 的修改会被记录,最终生成新状态
draft.count = 1;
draft.list.push(4);
});
// state 保持不变(不可变)
console.log(state); // { count: 0, list: [1, 2, 3] }
console.log(nextState); // { count: 1, list: [1, 2, 3, 4] }
// 结构共享:未变化的部分共享引用
state.list !== nextState.list; // true(list被修改)
实现原理:Proxy代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 简化版Immer实现
function produce(base, recipe) {
const changed = new Map(); // 记录修改
const proxies = new Map(); // 缓存代理
// 创建代理
function createProxy(target) {
if (proxies.has(target)) return proxies.get(target);
const proxy = new Proxy(target, {
get(obj, key) {
// 返回嵌套对象的代理
const value = obj[key];
if (typeof value === 'object' && value !== null) {
return createProxy(value);
}
return value;
},
set(obj, key, value) {
changed.set(obj, true); // 标记为已修改
obj[key] = value;
return true;
}
});
proxies.set(target, proxy);
return proxy;
}
// 执行修改
const draft = createProxy(base);
recipe(draft);
// 生成新状态(结构共享)
function finalize(obj) {
if (!changed.has(obj)) return obj; // 未修改,返回原引用
const clone = Array.isArray(obj) ? [...obj] : { ...obj };
for (const key in clone) {
if (typeof clone[key] === 'object') {
clone[key] = finalize(clone[key]); // 递归处理
}
}
return clone;
}
return finalize(base);
}
Immer的核心流程
1
2
3
4
5
6
7
8
9
10
1. 创建Draft:Proxy包装原始状态
state → draft(代理对象)
2. 执行Recipe:在draft上"直接修改"
draft.count = 1; // 实际是记录修改
3. Finalize:生成新状态
- 检测哪些节点被修改
- 只复制修改路径上的节点
- 共享未修改的节点
自动冻结(Object.freeze)
1
2
3
4
5
6
7
8
9
10
11
import { produce, enableFreeze } from 'immer';
// Immer默认在生产环境自动冻结结果
enableFreeze(); // 显式启用
const state = { a: 1 };
const newState = produce(state, draft => {
draft.a = 2;
});
newState.a = 3; // 报错!Cannot assign to read only property
4. React中的不可变数据
为什么React需要不可变数据?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Counter() {
const [state, setState] = useState({ count: 0, list: [] });
// ❌ 错误:直接修改state
const badIncrement = () => {
state.count += 1;
setState(state); // 引用未变,不会触发渲染
};
// ✅ 正确:创建新对象
const goodIncrement = () => {
setState(prev => ({ ...prev, count: prev.count + 1 }));
};
// ✅ 使用Immer
const immerIncrement = () => {
setState(produce(state, draft => {
draft.count += 1;
}));
};
}
React.memo与useMemo依赖引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// React.memo:只有props引用变化才重新渲染
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{data.name}</div>;
});
// ❌ 直接修改props.data,不会触发重渲染
data.name = 'new';
// ✅ 创建新对象,触发重渲染
const newData = { ...data, name: 'new' };
// useMemo:依赖项引用变化才重新计算
const result = useMemo(() => {
return heavyCompute(data);
}, [data]); // data引用变化才重新计算
5. Vue中的不可变数据需求
Vue 2:Object.defineProperty的局限
1
2
3
4
5
6
7
8
// Vue 2响应式:无法检测新增属性
const vm = new Vue({
data: { user: { name: 'Alice' } }
});
vm.user.name = 'Bob'; // ✅ 可检测
vm.user.age = 25; // ❌ 无法检测新增属性
Vue.set(vm.user, 'age', 25); // ✅ 需要手动调用
Vue 3:Proxy响应式
1
2
3
4
5
6
7
8
9
10
11
12
13
import { reactive } from 'vue';
const state = reactive({
user: { name: 'Alice' }
});
// Proxy可以检测新增属性
state.user.age = 25; // ✅ 自动检测
// 但数组直接修改索引仍然有问题
const arr = reactive([1, 2, 3]);
arr[0] = 10; // ✅ Vue 3能检测
arr.length = 0; // ✅ Vue 3能检测
Vue的不可变数据最佳实践
1
2
3
4
5
6
7
8
9
// ❌ 直接修改(虽然Vue 3能检测,但不推荐)
state.user.name = 'Bob';
// ✅ 创建新对象(推荐)
state.user = { ...state.user, name: 'Bob' };
// 使用Vue的reactive + 不可变更新
const state = reactive({ list: [] });
state.list = [...state.list, newItem]; // 不可变更新
实战案例
案例1:实现一个简易的状态管理(Redux模式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function createStore(reducer, initialState) {
let state = initialState;
const listeners = [];
function getState() {
return state;
}
function dispatch(action) {
// reducer必须返回新状态(不可变)
state = reducer(state, action);
listeners.forEach(fn => fn());
}
function subscribe(listener) {
listeners.push(listener);
return () => {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
return { getState, dispatch, subscribe };
}
// Reducer:必须是纯函数,返回新状态
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 }; // 不可变更新
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
};
const store = createStore(counterReducer, { count: 0 });
案例2:复杂嵌套状态更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { produce } from 'immer';
const initialState = {
users: {
byId: {
'1': { id: '1', name: 'Alice', posts: [1, 2] },
'2': { id: '2', name: 'Bob', posts: [3] }
},
allIds: ['1', '2']
},
posts: {
byId: {
1: { id: 1, title: 'Hello', likes: 10 },
2: { id: 2, title: 'World', likes: 5 },
3: { id: 3, title: 'Test', likes: 0 }
},
allIds: [1, 2, 3]
}
};
// 给用户1的帖子1点赞
const newState = produce(initialState, draft => {
draft.posts.byId[1].likes += 1;
draft.users.byId['1'].postCount = draft.users.byId['1'].posts.length;
});
// 未变化的部分共享引用
initialState.posts.byId[2] === newState.posts.byId[2]; // true
initialState.users.byId['2'] === newState.users.byId['2']; // true
案例3:列表性能优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { produce } from 'immer';
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', done: false },
{ id: 2, text: 'Learn Immer', done: false }
]);
// ✅ 使用Immer切换完成状态
const toggleTodo = (id) => {
setTodos(produce(todos, draft => {
const todo = draft.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}));
};
// ❌ 不使用Immer的繁琐写法
const toggleTodoMutable = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, done: !todo.done }
: todo
));
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.text} - {todo.done ? '✓' : '○'}
</li>
))}
</ul>
);
}
底层原理
Immer vs Immutable.js
| 特性 | Immer | Immutable.js |
|---|---|---|
| API风格 | 原生JS对象 | 自定义数据结构 |
| 学习成本 | 低 | 高(需学习新API) |
| 性能 | 中等(Proxy开销) | 高(专门优化的数据结构) |
| 结构共享 | 自动 | 自动 |
| 序列化 | 原生JSON | 需转换 |
| 包体积 | 16KB | 56KB |
结构共享的Trie树实现
Immutable.js使用哈希数组映射前缀树(HAMT)实现高效的结构共享:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 简化版HAMT概念
class TrieNode {
constructor() {
this.children = {}; // 子节点
this.value = null; // 叶节点存储值
}
}
// 插入路径:a.b.c = value
function insert(root, path, value) {
let node = root;
for (const key of path) {
if (!node.children[key]) {
node.children[key] = new TrieNode();
}
node = node.children[key];
}
node.value = value;
}
// 路径压缩:共享前缀
// { a: { b: 1 } } 和 { a: { b: 2, c: 3 } }
// 共享 a 节点
Copy-on-Write(写时复制)
Immer的核心思想是Copy-on-Write:
1
2
3
4
5
6
7
8
9
// 只有修改时才复制
function getOrCreateChild(parent, key) {
if (parent[key] && !isModified(parent[key])) {
return parent[key]; // 共享
}
const child = shallowCopy(parent[key]); // 复制
parent[key] = child;
return child;
}
高频面试题解析
Q1:为什么React不能直接修改state?
答案:
- 引用相等判断:React使用
Object.is(prev, next)判断状态是否变化,直接修改不改变引用,不会触发渲染 - shouldComponentUpdate优化:React.memo、PureComponent依赖浅比较,直接修改会绕过优化
- 并发模式安全:React 18的并发渲染需要状态快照,直接修改会导致竞态条件
Q2:Immer是怎么实现结构共享的?
答案:
- 使用Proxy代理原始对象,拦截所有读写操作
- 记录修改路径,标记哪些节点被修改
- 生成新状态时,只复制修改路径上的节点,共享未修改的节点
- 最终结果自动Object.freeze冻结(生产环境)
Q3:什么场景应该用Immer?
答案: 适用场景:
- 深层嵌套状态更新(如Redux reducer)
- 复杂表单状态管理
- 不想写繁琐的展开运算符代码
不适用场景:
- 极致性能要求(Proxy有开销)
- 状态非常简单(浅层对象)
- 需要兼容不支持Proxy的环境
Q4:Object.assign和展开运算符有什么区别?
答案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 基本等价:都是浅拷贝
const a = Object.assign({}, obj, { key: value });
const b = { ...obj, key: value };
// 区别1:Object.assign会触发setter
const obj = {
set foo(value) { console.log('setter called'); }
};
Object.assign(obj, { foo: 1 }); // 触发setter
{ ...obj, foo: 1 }; // 不触发
// 区别2:展开运算符不能展开undefined/null
{ ...null }; // {}
Object.assign({}, null); // 不报错
Q5:手写一个简单的结构共享更新函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function updateImmutable(obj, path, value) {
if (path.length === 0) return value;
const [key, ...rest] = path;
const newObj = Array.isArray(obj) ? [...obj] : { ...obj };
newObj[key] = updateImmutable(obj[key] || {}, rest, value);
return newObj;
}
// 使用
const state = { a: { b: { c: 1 } } };
const newState = updateImmutable(state, ['a', 'b', 'c'], 2);
// state.a.b.c = 1, newState.a.b.c = 2
// state.a === newState.a → false(a.b路径上的节点都被复制)
总结与扩展
核心要点回顾
- 不可变数据:创建后永不修改,变更返回新引用
- 必要性:引用相等判断、时间旅行调试、并发安全
- 结构共享:只复制修改路径,共享未变部分,实现高效不可变更新
- Immer.js:Proxy + Copy-on-Write,让不可变更新像直接修改一样简单
- 框架应用:React的状态更新、Redux的reducer、Vue 3的响应式都需要不可变数据
深入学习资源
相关主题
- 深拷贝与浅拷贝的实现
- Redux中间件原理
- Vue 3响应式原理
- 函数式编程范式
不可变数据是现代前端框架的基石。理解它,才能真正理解React的渲染机制、Redux的设计哲学、以及Vue 3的响应式原理。掌握Immer,让复杂状态更新变得简单高效。
本文由作者按照 CC BY 4.0 进行授权