13.4 Immutable 模式与 Immer 库
不可变数据(Immutable Data)概念
什么是不可变数据?
不可变数据是指一旦创建就不能被修改的数据结构。任何对不可变数据的"修改"操作都会返回一个新的数据结构,而原始数据保持不变。
// 可变数据操作(直接修改原对象)
const mutableObj = { a: 1, b: 2 };
mutableObj.a = 3; // 直接修改原对象
// 不可变数据操作(返回新对象)
const immutableObj = { a: 1, b: 2 };
const newObj = { ...immutableObj, a: 3 }; // 创建新对象
为什么需要不可变数据?
- 可预测性:数据流动更清晰,避免意外的副作用
- 性能优化:通过引用比较可以快速检测数据变化
- 时间旅行调试:保存应用状态的完整历史
- 并发安全:避免多线程/异步操作中的数据竞争
原生JavaScript实现不可变更新
对象更新
const state = {
user: {
name: 'Alice',
age: 25,
address: {
city: 'New York',
zip: '10001'
}
},
settings: {
theme: 'light'
}
};
// 更新嵌套属性 - 原生方式
const newState = {
...state,
user: {
...state.user,
age: 26,
address: {
...state.user.address,
zip: '10002'
}
}
};
数组更新
const todos = [
{ id: 1, text: 'Learn JavaScript', done: false },
{ id: 2, text: 'Learn React', done: true }
];
// 添加元素
const newTodos = [...todos, { id: 3, text: 'Learn Redux', done: false }];
// 更新元素
const updatedTodos = todos.map(todo =>
todo.id === 2 ? { ...todo, done: false } : todo
);
// 删除元素
const filteredTodos = todos.filter(todo => todo.id !== 1);
原生方式的痛点
- 嵌套结构更新代码冗长
- 容易遗漏
...扩展运算符 - 大型数据结构性能较差
- 代码可读性低
Immer 库介绍
Immer 是一个让不可变数据操作变得更简单的库,它允许你使用可变的方式编写代码,但实际上执行的是不可变更新。
基本用法
import produce from 'immer';
const baseState = {
user: {
name: 'Alice',
age: 25
},
todos: []
};
const nextState = produce(baseState, draftState => {
draftState.user.age = 26;
draftState.todos.push({ text: 'Learn Immer' });
});
工作原理
- current state:接收当前状态
- draft state:创建代理对象(草稿状态)
- modifications:对草稿进行可变操作
- next state:根据草稿生成新的不可变状态
核心API
- produce(baseState, recipe):基本用法
- produce(recipe):柯里化版本,创建生产者函数
// 柯里化版本
const incrementAge = produce(draft => {
draft.user.age++;
});
const newState = incrementAge(baseState);
Immer 高级用法
使用ES6 Map和Set
const state = {
map: new Map([['key1', 'value1']]),
set: new Set([1, 2, 3])
};
const nextState = produce(state, draft => {
draft.map.set('key2', 'value2');
draft.set.add(4);
});
处理不可变数据的数组操作
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
const nextUsers = produce(users, draft => {
// 修改
draft[0].name = 'Alice Smith';
// 添加
draft.push({ id: 3, name: 'Charlie' });
// 删除
draft.splice(1, 1); // 删除Bob
});
异步生产者函数
async function updateUser(api, userId, changes) {
return produce(await api.getUser(userId), async draft => {
draft.changes = changes;
await api.updateUser(draft);
});
}
与React一起使用
function reducer(state, action) {
return produce(state, draft => {
switch (action.type) {
case 'ADD_TODO':
draft.todos.push(action.payload);
break;
case 'TOGGLE_TODO':
const todo = draft.todos.find(t => t.id === action.id);
todo.done = !todo.done;
break;
// ...其他cases
}
});
}
性能优化
结构共享
Immer会自动复用未修改的部分,减少内存使用:
const original = { a: 1, b: { deep: { value: 2 } } };
const updated = produce(original, draft => {
draft.a = 3;
});
console.log(original.b === updated.b); // true (b对象被复用)
console.log(original.b.deep === updated.b.deep); // true (deep对象被复用)
冻结状态
Immer在开发环境下会自动冻结状态,帮助捕获意外修改:
const state = { a: 1 };
const nextState = produce(state, draft => { draft.a = 2 });
// 在开发环境下会抛出错误
nextState.a = 3; // Error: Cannot assign to read only property 'a' of object '#<Object>'
性能比较
- 小对象:Immer比手动扩展运算符稍慢
- 大对象/嵌套结构:Immer性能更好
- 高频更新:考虑使用原生不可变数据结构
替代方案比较
Immutable.js
import { Map } from 'immutable';
const data = Map({ a: 1, b: 2 });
const newData = data.set('a', 3);
优点:
- 性能极佳
- 丰富的API
缺点:
- 学习曲线陡峭
- 需要整个项目采用其数据结构
- 与普通JS对象互操作需要转换
Immer
优点:
- 使用普通JS对象和数组
- 更直观的API
- 渐进式采用
缺点:
- 对超大数据集可能不如Immutable.js高效
- 需要了解代理机制
最佳实践
- Redux reducer:使用Immer简化reducer逻辑
- React状态更新:与useState/useReducer结合
- 复杂对象操作:深度嵌套结构的更新
- 避免的场景:
- 极高性能要求的场景
- 非常简单的状态更新
- 已经使用Immutable.js的项目
实战示例
示例1:购物车更新
const initialState = {
items: [],
total: 0
};
function cartReducer(state = initialState, action) {
return produce(state, draft => {
switch (action.type) {
case 'ADD_ITEM':
const existingItem = draft.items.find(item => item.id === action.payload.id);
if (existingItem) {
existingItem.quantity++;
} else {
draft.items.push({ ...action.payload, quantity: 1 });
}
draft.total += action.payload.price;
break;
case 'REMOVE_ITEM':
const index = draft.items.findIndex(item => item.id === action.payload);
if (index !== -1) {
draft.total -= draft.items[index].price * draft.items[index].quantity;
draft.items.splice(index, 1);
}
break;
case 'UPDATE_QUANTITY':
const item = draft.items.find(item => item.id === action.payload.id);
if (item) {
draft.total += (action.payload.quantity - item.quantity) * item.price;
item.quantity = action.payload.quantity;
}
break;
}
});
}
示例2:表单状态管理
function Form() {
const [form, setForm] = useState({
username: '',
profile: {
firstName: '',
lastName: '',
address: {
street: '',
city: ''
}
}
});
const handleChange = (path, value) => {
setForm(produce(draft => {
let current = draft;
for (let i = 0; i < path.length - 1; i++) {
current = current[path[i]];
}
current[path[path.length - 1]] = value;
}));
};
// 使用示例
// handleChange(['username'], 'Alice');
// handleChange(['profile', 'firstName'], 'Alice');
// handleChange(['profile', 'address', 'city'], 'New York');
}
示例3:游戏状态更新
const gameState = {
players: [
{ id: 1, name: 'Alice', score: 0, items: ['sword'] },
{ id: 2, name: 'Bob', score: 0, items: ['shield'] }
],
currentTurn: 1,
board: {
// ...复杂棋盘状态
}
};
function applyMove(gameState, move) {
return produce(gameState, draft => {
const player = draft.players.find(p => p.id === move.playerId);
player.score += move.points;
if (move.collectItem) {
player.items.push(move.collectItem);
}
if (move.useItem) {
const itemIndex = player.items.indexOf(move.useItem);
if (itemIndex !== -1) {
player.items.splice(itemIndex, 1);
}
}
draft.currentTurn = draft.players.findIndex(p => p.id === move.playerId) + 1;
// 更新复杂棋盘状态...
});
}
总结
不可变数据模式是现代JavaScript开发中的重要概念:
-
Immutable模式:
- 确保状态变更可预测
- 便于追踪变化和调试
- 优化React等框架的性能
-
Immer库:
- 使用可变语法实现不可变更新
- 简化复杂嵌套结构的操作
- 自动结构共享减少内存使用
- 开发时冻结状态帮助调试
-
适用场景:
- Redux reducers
- React状态管理
- 复杂数据结构的操作
- 需要历史记录/撤销重做的功能
通过合理使用Immer,可以在保持不可变数据优点的同时,显著减少样板代码,提高开发效率和代码可维护性。
#前端开发
分享于 2025-03-25
上一篇:13.3 高阶函数与闭包应用
下一篇:14.1 自定义错误类型(继承 Error)