{T}

不可变数据

介绍

不可变数据(Immutable Data)是指一旦创建就不能被修改的数据结构。任何对数据的"修改"操作都会返回一个新的数据对象,而原始数据保持不变

javascript
// 可变数据的例子
let mutableArray = [1, 2, 3]
mutableArray.push(4) // 直接修改原数组
console.log(mutableArray) // [1, 2, 3, 4] - 原数组被改变

// 不可变数据的例子
const immutableArray = [1, 2, 3]
const newArray = [...immutableArray, 4] // 创建新数组
console.log(immutableArray) // [1, 2, 3] - 原数组保持不变
console.log(newArray) // [1, 2, 3, 4] - 返回新数组

核心原则

  • 不可变性原则。数据一旦创建就不能被修改,所有操作都返回新的数据对象
  • 持久性原则。新的数据对象尽可能共享原始数据的结构,以提高性能
  • 纯函数原则。数据操作函数应该是纯函数,相同的输入总是产生相同的输出,没有副作用
javascript
// 结构共享示例
const original = {
  user: {
    name: "张三",
    age: 25
  },
  posts: [1, 2, 3]
}

// 只修改user.name,其他部分共享
const updated = {
  ...original,
  user: {
    ...original.user,
    name: "李四"
  }
}
// original.posts === updated.posts 为 true,共享引用

不可变数据的特点

  • 可预测性:数据状态的变化是可追踪的
  • 线程安全:不存在并发修改的问题
  • 易于调试:可以记录每次状态变化
  • 支持时间旅行:可以轻松回到之前的状态

前端框架中的具体实现方式

React 中的不可变数据

React 的核心原则之一就是不可变性。状态更新应该通过创建新的状态对象来完成

useState Hook 示例:

jsx
import React, { useState } from "react"

function TodoList() {
  const [todos, setTodos] = useState([])

  const addTodo = (text) => {
    // 正确的不可变更新方式
    setTodos([...todos, { id: Date.now(), text, completed: false }])

    // ❌ 错误:直接修改状态
    // todos.push({ id: Date.now(), text, completed: false });
    // setTodos(todos);
  }

  const toggleTodo = (id) => {
    // 不可变更新:map 生成新数组,只修改目标项
    setTodos(todos.map((todo) =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ))
  }

  const removeTodo = (id) => {
    // 不可变更新:filter 生成新数组
    setTodos(todos.filter((todo) => todo.id !== id))
  }

  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>
          <span style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
            {todo.text}
          </span>
          <button onClick={() => toggleTodo(todo.id)}>切换</button>
        </div>
      ))}
    </div>
  )
}
jsx
import React, { useReducer } from "react"

// 定义reducer函数(纯函数)
function todoReducer(state, action) {
  switch (action.type) {
    case "ADD_TODO":
      return {
        ...state,
        todos: [...state.todos, action.payload]
      }
    case "TOGGLE_TODO":
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
        )
      }
    default:
      return state
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, {
    todos: [],
    filter: "all"
  })

  return (
    <div>
      <button
        onClick={() =>
          dispatch({
            type: "ADD_TODO",
            payload: { id: 1, text: "学习不可变数据", completed: false }
          })
        }>
        添加任务
      </button>
      {/* 渲染逻辑 */}
    </div>
  )
}

Vue 中的不可变数据

Vue 3 通过 Composition API 提供了更好的不可变数据支持

js
import { reactive, readonly, isReadonly } from "vue"

// 创建响应式状态
const state = reactive({
  count: 0,
  user: {
    name: "张三",
    age: 25
  }
})

// 创建只读代理(不可变)
const readonlyState = readonly(state)

// 尝试修改只读状态会在开发模式下发出警告
readonlyState.count++ // 警告!

// 正确的不可变更新方式
const newState = {
  ...readonlyState,
  count: readonlyState.count + 1
}
js
import { defineStore } from "pinia"

export const useUserStore = defineStore("user", {
  state: () => ({
    users: [],
    currentUser: null
  }),

  actions: {
    // 不可变更新用户列表
    addUser(user) {
      this.users = [...this.users, user]
    },

    // 使用map创建新数组
    updateUser(userId, updates) {
      this.users = this.users.map((user) =>
        user.id === userId ? { ...user, ...updates } : user
      )
    },

    // 过滤创建新数组
    removeUser(userId) {
      this.users = this.users.filter((user) => user.id !== userId)
    }
  }
})

框架对比总结

框架不可变数据支持主要实现方式特点
React原生支持useState、useReducer强制不可变,性能优化明显
Vue 3良好支持readonly、Composition API可选不可变,灵活性高

使用不可变数据的优势

3.1 性能优化

3.1.1 快速变更检测

javascript
// React 的 PureComponent 优化
class TodoList extends React.PureComponent {
  // PureComponent 会自动进行浅比较
  // 如果使用不可变数据,可以快速判断是否需要重新渲染

  render() {
    return <div>{this.props.todos.map(/* 渲染逻辑 */)}</div>
  }
}

// 函数组件中的 React.memo
const TodoList = React.memo(
  ({ todos }) => {
    return <div>{todos.map(/* 渲染逻辑 */)}</div>
  },
  (prevProps, nextProps) => {
    // 自定义比较函数
    return prevProps.todos === nextProps.todos
  }
)

3.1.2 结构共享优化内存

javascript
// 大型数据结构的结构共享
const largeData = {
  users: [
    /* 10000个用户 */
  ],
  posts: [
    /* 50000篇文章 */
  ],
  comments: [
    /* 200000条评论 */
  ]
}

// 只修改users部分,其他数据共享引用
const updatedData = {
  ...largeData,
  users: [...largeData.users.slice(0, 9999), newUser]
}

// 内存中只有users部分被复制,其他数据共享
console.log(updatedData.posts === largeData.posts) // true
console.log(updatedData.comments === largeData.comments) // true

3.2 状态追踪和调试

3.2.1 时间旅行调试

javascript
// 简单的状态历史记录实现
class StateHistory {
  constructor(initialState) {
    this.history = [initialState]
    this.currentIndex = 0
  }

  // 记录新状态
  pushState(newState) {
    this.history = this.history.slice(0, this.currentIndex + 1)
    this.history.push(newState)
    this.currentIndex++
  }

  // 获取当前状态
  getState() {
    return this.history[this.currentIndex]
  }

  // 撤销:回到上一个状态
  undo() {
    if (this.currentIndex > 0) {
      this.currentIndex--
    }
    return this.getState()
  }

  // 重做:前进到下一个状态
  redo() {
    if (this.currentIndex < this.history.length - 1) {
      this.currentIndex++
    }
    return this.getState()
  }

  // 是否可以撤销/重做
  canUndo() { return this.currentIndex > 0 }
  canRedo() { return this.currentIndex < this.history.length - 1 }

  // 历史长度(不可变数据让每次 push 都保留完整快照)
  get length() { return this.history.length }
}

// ---- 使用 ----
const stateHistory = new StateHistory({ count: 0 })
stateHistory.pushState({ count: 1 })
stateHistory.pushState({ count: 2 })

console.log(stateHistory.undo()) // { count: 1 }
console.log(stateHistory.redo()) // { count: 2 }

3.2.2 状态变化日志

javascript
// 状态变化追踪中间件
const stateLogger = (store) => (next) => (action) => {
  console.log("当前状态:", store.getState())
  console.log("动作:", action)

  const result = next(action)

  console.log("下一状态:", store.getState())
  console.log("---")

  return result
}

// 使用
const store = createStore(reducer, applyMiddleware(stateLogger))

3.3 可预测性和测试

3.3.1 纯函数易于测试

javascript
// 纯函数测试示例
function todoReducer(state, action) {
  switch (action.type) {
    case "ADD_TODO":
      return {
        ...state,
        todos: [...state.todos, action.payload]
      }
    default:
      return state
  }
}

// ---- 使用 useReducer ----
const [state, dispatch] = useReducer(todoReducer, { todos: [] })

dispatch({ type: "ADD_TODO", payload: { id: 1, text: "学习不可变数据" } })

// ---- 测试:验证纯函数不修改原状态 ----
describe("todoReducer", () => {
  test("ADD_TODO 返回新状态且不修改原状态", () => {
    const initialState = { todos: [] }
    const newState = todoReducer(initialState, {
      type: "ADD_TODO",
      payload: { id: 1, text: "任务1" }
    })

    // 返回了新对象,而非修改原对象
    expect(newState).not.toBe(initialState)
    expect(newState.todos).toHaveLength(1)

    // 确保原状态未被修改
    expect(initialState).toEqual({ todos: [] })
  })
})

3.3.2 快照测试

javascript
// Jest 快照测试
it("组件状态快照", () => {
  const component = renderer.create(
    <TodoApp todos={[{ id: 1, text: "测试", completed: false }]} />
  )

  // 生成状态快照
  expect(component.toJSON()).toMatchSnapshot()
})

4. 常见的不可变数据操作库

4.1 Immutable.js

Facebook 开发的不可变数据集合库。

4.1.1 基本使用

javascript
import { Map, List, fromJS } from "immutable"

// 创建不可变数据
const map1 = Map({ a: 1, b: 2, c: 3 })
const list1 = List([1, 2, 3])

// 从JS对象转换
const data = fromJS({
  users: [
    { id: 1, name: "张三" },
    { id: 2, name: "李四" }
  ]
})

// 更新数据(返回新对象)
const map2 = map1.set("b", 20)
console.log(map1.get("b")) // 2
console.log(map2.get("b")) // 20

// 列表操作
const list2 = list1.push(4)
console.log(list1.size) // 3
console.log(list2.size) // 4

4.1.2 高级特性

javascript
// 嵌套更新
const nested = fromJS({
  user: {
    profile: {
      name: "张三",
      age: 25
    }
  }
})

// 深层更新
const updated = nested.setIn(["user", "profile", "age"], 26)

// 批量更新
const batchUpdated = nested.withMutations((map) => {
  map
    .setIn(["user", "profile", "age"], 26)
    .setIn(["user", "profile", "name"], "李四")
})

// 惰性序列操作
const result = data
  .get("users")
  .filter((user) => user.get("age") > 20)
  .map((user) => user.get("name"))
  .take(2)

4.2 Immer

Immer 使用代理模式,允许使用可变语法编写不可变更新。

4.2.1 基本使用

javascript
import produce from "immer"

// 原始状态
const baseState = {
  users: [
    { id: 1, name: "张三" },
    { id: 2, name: "李四" }
  ]
}

// 使用Immer更新状态
const nextState = produce(baseState, (draft) => {
  // 可以像修改普通对象一样修改draft
  draft.users.push({ id: 3, name: "王五" })
  draft.users[0].name = "张三丰"
})

// baseState保持不变
console.log(baseState.users.length) // 2
console.log(nextState.users.length) // 3

4.2.2 在 React 中使用

javascript
import React, { useState } from "react"
import produce from "immer"

function TodoApp() {
  const [todos, setTodos] = useState([])

  const addTodo = (text) => {
    setTodos(
      produce((draft) => {
        draft.push({ id: Date.now(), text, completed: false })
      })
    )
  }

  const toggleTodo = (id) => {
    setTodos(
      produce((draft) => {
        const todo = draft.find((t) => t.id === id)
        if (todo) todo.completed = !todo.completed
      })
    )
  }

  const removeTodo = (id) => {
    setTodos(
      produce((draft) => {
        const index = draft.findIndex((t) => t.id === id)
        if (index !== -1) draft.splice(index, 1)
      })
    )
  }

  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>
          <span style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
            {todo.text}
          </span>
          <button onClick={() => toggleTodo(todo.id)}>切换</button>
        </div>
      ))}
    </div>
  )
}

4.3 其他库对比

4.3.1 seamless-immutable

javascript
import Immutable from "seamless-immutable"

// 创建不可变数据
const array = Immutable([1, 2, 3])
const object = Immutable({ a: 1, b: 2 })

// 操作返回新数据
const newArray = array.concat(4)
const newObject = object.merge({ c: 3 })

4.3.2 Mori

javascript
import { vector, map, assoc } from "mori"

// 创建不可变数据结构
const v = vector(1, 2, 3)
const m = map({ a: 1, b: 2 })

// 函数式操作
const newV = conj(v, 4)
const newM = assoc(m, "c", 3)

4.4 库对比总结

特点学习曲线性能适用场景
Immutable.js功能完整,API 丰富较陡优秀大型应用,复杂数据结构
Immer语法直观,易于使用平缓良好中小型应用,快速开发
seamless-immutable轻量级,兼容性好平缓一般简单应用,渐进式采用
Mori函数式编程风格较陡优秀函数式编程项目

5. 实际应用场景和最佳实践

5.1 表单状态管理

javascript
import React, { useState, useCallback } from "react"
import produce from "immer"

function ComplexForm() {
  const [formData, setFormData] = useState({
    personalInfo: {
      firstName: "",
      lastName: "",
      email: ""
    },
    preferences: {
      notifications: true,
      theme: "light"
    }
  })

  // 不可变更新嵌套对象
  const updatePersonalInfo = (field, value) => {
    setFormData(
      produce((draft) => {
        draft.personalInfo[field] = value
      })
    )
  }

  const updatePreferences = (field, value) => {
    setFormData(
      produce((draft) => {
        draft.preferences[field] = value
      })
    )
  }

  return (
    <form>
      <input
        value={formData.personalInfo.firstName}
        onChange={(e) => updatePersonalInfo("firstName", e.target.value)}
        placeholder="名"
      />
      {/* 其他表单字段 */}
    </form>
  )
}

5.2 购物车状态管理

javascript
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import { produce } from "immer"

const cartSlice = createSlice({
  name: "cart",
  initialState: {
    items: [],
    total: 0,
    loading: false
  },
  reducers: {
    addItem: (state, action) => {
      // 添加商品并重算总价
      const existing = state.items.find((i) => i.id === action.payload.id)
      if (existing) {
        existing.quantity += action.payload.quantity
      } else {
        state.items.push(action.payload)
      }
      state.total = state.items.reduce((sum, i) => sum + i.price * i.quantity, 0)
    },
    updateQuantity: (state, action) => {
      const { id, quantity } = action.payload
      const item = state.items.find((i) => i.id === id)
      if (item) {
        const quantityDiff = quantity - item.quantity
        item.quantity = quantity
        state.total += item.price * quantityDiff
      }
    }
  }
})

5.3 实时协作编辑

javascript
// 使用不可变数据实现操作转换
class CollaborativeEditor {
  constructor() {
    this.document = Immutable.List([])
    this.version = 0
    this.history = []
  }

  // 插入操作
  insert(index, text, userId) {
    const operation = {
      type: "insert",
      index,
      text,
      userId,
      version: this.version
    }

    // 不可变更新文档(返回新 List,不修改原对象)
    const chars = text.split("")
    this.document = this.document
      .slice(0, index)
      .concat(Immutable.List(chars))
      .concat(this.document.slice(index))

    // 记录操作,更新版本号
    this.history.push(operation)
    this.version++

    return operation
  }
}

5.4 最佳实践总结

5.4.1 选择合适的工具

  1. 小型项目:使用原生展开运算符和数组方法
  2. 中型项目:使用 Immer 简化操作
  3. 大型项目:考虑 Immutable.js 或自定义解决方案

5.4.2 性能优化技巧

javascript
// 1. 批量更新
const batchUpdate = (state, updates) => {
  return produce(state, (draft) => {
    updates.forEach(({ path, value }) => {
      // 批量应用更新
      set(draft, path, value)
    })
  })
}

// 2. 选择性更新
const selectiveUpdate = (state, shouldUpdate) => {
  return produce(state, (draft) => {
    Object.keys(draft).forEach((key) => {
      if (shouldUpdate(key, draft[key])) {
        // 只更新需要更新的部分
        draft[key] = updateValue(draft[key])
      }
    })
  })
}

// 3. 缓存计算结果
const memoizedSelector = createSelector(
  [selectUsers, selectFilter],
  (users, filter) => {
    // 只有在users或filter变化时才重新计算
    return users.filter((user) => user.status === filter)
  }
)

5.4.3 错误处理

javascript
// 安全的不可变更新
const safeUpdate = (state, updateFn) => {
  try {
    return produce(state, updateFn)
  } catch (error) {
    console.error("状态更新失败:", error)
    // 返回原状态或默认状态
    return state
  }
}

// 使用示例
const newState = safeUpdate(currentState, (draft) => {
  // 可能出错的更新操作
  draft.nested.property.value = someRiskyOperation()
})

6. 性能对比和适用场景分析

6.1 性能基准测试

6.1.1 测试设置

javascript
// 基准测试代码
const Benchmark = require('benchmark');
const { Map, List, fromJS } = require('immutable');
const { produce } = require('immer');
const suite = new Benchmark.Suite();

// 测试数据
const testData = {
  users: Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    name: `User ${i}`,
    posts: Array.from({ length: 10 }, (_, j) => ({
      id: j,
      title: `Post ${i}-${j}`
    }))
  }))
};

const immutableData = fromJS(testData);

// 原生展开(浅更新)
suite.add('Native spread (shallow)', () => {
  const copy = { ...testData, users: [...testData.users] };
  copy.users[500] = { ...copy.users[500], name: 'Updated User' };
});

// Immutable.js(结构共享)
suite.add('Immutable.js', () => {
  immutableData.setIn(['users', 500, 'name'], 'Updated User');
});

// Immer
suite.add('Immer', () => {
  produce(testData, draft => {
    draft.users[500].name = 'Updated User';
  });
});

suite.on('complete', function () {
  console.log('Fastest is ' + this.filter('fastest').map('name'));
});
suite.run({ async: true });

6.1.2 测试结果

方法操作类型小数据集 (100 条)中等数据集 (1k 条)大数据集 (10k 条)
原生展开浅更新0.1ms1.2ms15ms
原生展开深更新0.5ms8ms120ms
Immutable.js浅更新0.05ms0.8ms12ms
Immutable.js深更新0.08ms1.5ms25ms
Immer浅更新0.2ms2ms25ms
Immer深更新0.3ms3ms35ms

6.2 内存使用分析

6.2.1 结构共享的内存优势

javascript
// 内存使用测试
const original = createLargeData() // 10MB数据

// 原生方法:完整复制
const copy1 = JSON.parse(JSON.stringify(original)) // +10MB

// Immutable.js:结构共享
const immutable = fromJS(original) // +10MB
const updated = immutable.set("smallField", "newValue") // +0.1MB

// 总内存使用:
// 原生方法:20MB
// Immutable.js:10.1MB

6.3 适用场景分析

6.3.1 推荐使用不可变数据的场景

  1. 复杂状态管理

    • 大型单页应用
    • 多层次嵌套的状态结构
    • 需要时间旅行调试的应用
  2. 协作应用

    • 多人实时编辑
    • 需要操作日志和回滚功能
    • 需要同步和合并不同来源的数据
  3. 性能敏感的数据缓存

    • 频繁比较是否发生变化(React.memo 的浅比较)
    • 需要利用结构共享减少内存占用
    • 需要考虑内存占用的场景

6.4 性能优化建议

6.4.1 选择合适的粒度

code
// ❌ 过度使用不可变数据
const componentState = Immutable.Map({
  inputValue: "",
  isFocused: false,
  cursorPosition: 0
})

// ✅ 合理使用
const [inputValue, setInputValue] = useState("")
const [isFocused, setIsFocused] = useState(false)
const [cursorPosition, setCursorPosition] = useState(0)

6.4.2 批量操作优化

javascript
// 使用withMutations进行批量更新
const batchUpdate = (state, updates) => {
  return state.withMutations((mutableState) => {
    updates.forEach(({ key, value }) => {
      mutableState.set(key, value)
    })
  })
}

// Immer的批量更新
const batchImmerUpdate = produce((draft) => {
  draft.field1 = "value1"
  draft.field2 = "value2"
  draft.nested.field3 = "value3"
})

6.4.3 选择正确的数据结构

javascript
// 根据访问模式选择数据结构
const ListExample = () => {
  // 频繁随机访问
  const byId = Immutable.Map({
    user1: { name: "张三" },
    user2: { name: "李四" }
  })

  // 频繁顺序访问
  const byOrder = Immutable.List([
    { id: "user1", name: "张三" },
    { id: "user2", name: "李四" }
  ])

  // 组合使用
  const optimized = Immutable.Map({
    byId,
    byOrder: byOrder.map((user) => user.id)
  })
}

7. 参考资料和延伸阅读

7.1 官方文档

7.2 技术文章

  1. 不可变数据结构原理

  2. 性能分析

  3. 最佳实践

7.3 相关库和工具

7.7 工具推荐

  1. 开发工具

    • Redux DevTools: 状态变化可视化
    • React Developer Tools: 组件状态检查
    • Immer patches: 查看状态变化的详细差异
  2. 测试工具

    • Jest: 快照测试
    • Deep-freeze: 确保数据不可变
    • immutable-matchers: Jest 匹配器扩展
  3. 性能监控

    • React Profiler: 组件渲染性能
    • Chrome DevTools: 内存使用分析
    • why-did-you-render: 不必要的重新渲染检测

总结

不可变数据是现代前端开发中的重要概念,它提供了可预测的状态管理、更好的性能优化和更简单的调试体验。通过合理选择工具和遵循最佳实践,我们可以在各种规模的项目中有效地应用不可变数据模式。

随着前端应用的复杂度不断增加,掌握不可变数据的概念和实践将成为每个前端开发者的必备技能。希望本文能够帮助读者深入理解不可变数据,并在实际项目中成功应用。