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 }; // 创建新对象

为什么需要不可变数据?

  1. 可预测性:数据流动更清晰,避免意外的副作用
  2. 性能优化:通过引用比较可以快速检测数据变化
  3. 时间旅行调试:保存应用状态的完整历史
  4. 并发安全:避免多线程/异步操作中的数据竞争

原生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);

原生方式的痛点

  1. 嵌套结构更新代码冗长
  2. 容易遗漏...扩展运算符
  3. 大型数据结构性能较差
  4. 代码可读性低

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' });
});

工作原理

  1. current state:接收当前状态
  2. draft state:创建代理对象(草稿状态)
  3. modifications:对草稿进行可变操作
  4. next state:根据草稿生成新的不可变状态

核心API

  1. produce(baseState, recipe):基本用法
  2. 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>'

性能比较

  1. 小对象:Immer比手动扩展运算符稍慢
  2. 大对象/嵌套结构:Immer性能更好
  3. 高频更新:考虑使用原生不可变数据结构

替代方案比较

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高效
  • 需要了解代理机制

最佳实践

  1. Redux reducer:使用Immer简化reducer逻辑
  2. React状态更新:与useState/useReducer结合
  3. 复杂对象操作:深度嵌套结构的更新
  4. 避免的场景
    • 极高性能要求的场景
    • 非常简单的状态更新
    • 已经使用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开发中的重要概念:

  1. Immutable模式

    • 确保状态变更可预测
    • 便于追踪变化和调试
    • 优化React等框架的性能
  2. Immer库

    • 使用可变语法实现不可变更新
    • 简化复杂嵌套结构的操作
    • 自动结构共享减少内存使用
    • 开发时冻结状态帮助调试
  3. 适用场景

    • Redux reducers
    • React状态管理
    • 复杂数据结构的操作
    • 需要历史记录/撤销重做的功能

通过合理使用Immer,可以在保持不可变数据优点的同时,显著减少样板代码,提高开发效率和代码可维护性。

#前端开发 分享于 2025-03-25

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】