返回首页

说说 React 的 immutable 的理解?应用场景?

问题解析(面试官考察点)

面试官通过此问题主要考察:

  • 对不可变数据概念的理解
  • 对 JavaScript 中实现不可变数据的方式的了解
  • 对 Immutable.js 等库的使用
  • 对不可变数据在 React 中应用场景的认识
  • 对不可变数据带来的性能优化的理解

核心概念(基础知识点)

什么是 Immutable

Immutable(不可变)是指一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。

Immutable 的核心特性

  1. 不可变性: 数据创建后不能被修改
  2. 持久化数据结构: 新数据尽可能复用旧数据的结构
  3. 结构共享: 只修改变化的部分,未变化的部分共享引用

为什么需要 Immutable

JavaScript 中的对象和数组是引用类型,直接修改会带来以下问题:

  1. 意外的副作用: 修改一个对象可能影响其他地方
  2. 比较困难: 需要深度比较才能判断数据是否变化
  3. 不可预测性: 难以追踪数据的变化来源

详细解答(代码示例)

JavaScript 中的可变性问题

// 可变数据的问题
const user = {
  name: "张三",
  age: 25,
  address: {
    city: "北京",
    street: "长安街"
  }
};

// 浅拷贝的问题
const user2 = { ...user };
user2.address.city = "上海";

console.log(user.address.city); // "上海" - 原数据也被修改了!

// 数组的问题
const list = [1, 2, 3];
const list2 = list;
list2.push(4);

console.log(list); // [1, 2, 3, 4] - 原数组也被修改了!

手动实现不可变更新

// 对象的不可变更新
const user = {
  name: "张三",
  age: 25,
  address: {
    city: "北京",
    street: "长安街"
  }
};

// 浅层更新
const updatedUser = {
  ...user,
  age: 26
};

// 深层更新(需要手动展开每一层)
const updatedUserDeep = {
  ...user,
  address: {
    ...user.address,
    city: "上海"
  }
};

// 数组的不可变更新
const list = [1, 2, 3];

// 添加元素
const newList = [...list, 4];

// 插入元素
const insertedList = [...list.slice(0, 1), 99, ...list.slice(1)];

// 删除元素
const removedList = list.filter((_, index) => index !== 1);

// 修改元素
const modifiedList = list.map((item, index) =
  index === 1 ? 99 : item
);

// 复杂对象的不可变更新
const state = {
  users: [
    { id: 1, name: "张三", todos: [{ id: 1, text: "学习" }] },
    { id: 2, name: "李四", todos: [{ id: 2, text: "工作" }] }
  ],
  filter: "all"
};

// 更新特定用户的特定待办事项(非常繁琐)
const newState = {
  ...state,
  users: state.users.map(user =
    user.id === 1
      ? {
          ...user,
          todos: user.todos.map(todo =
            todo.id === 1 ? { ...todo, text: "学习 React" } : todo
          )
        }
      : user
  )
};

使用 Immutable.js

import { Map, List, fromJS, is } from "immutable";

// 创建 Immutable 对象
const map = Map({ a: 1, b: 2, c: 3 });
const list = List([1, 2, 3]);

// 从普通 JS 对象创建
const immutableUser = fromJS({
  name: "张三",
  age: 25,
  address: {
    city: "北京",
    street: "长安街"
  }
});

// 获取值
console.log(immutableUser.get("name")); // "张三"
console.log(immutableUser.getIn(["address", "city"])); // "北京"

// 更新值(返回新对象)
const updatedUser = immutableUser.set("age", 26);
const updatedUserDeep = immutableUser.setIn(["address", "city"], "上海");

// 原对象不变
console.log(immutableUser.get("age")); // 25
console.log(immutableUser.getIn(["address", "city"])); // "北京"

// 列表操作
const list = List([1, 2, 3]);
const newList = list.push(4); // [1, 2, 3, 4]
const insertedList = list.insert(1, 99); // [1, 99, 2, 3]
const removedList = list.delete(1); // [1, 3]

// 批量更新
const map = Map({ a: 1, b: 2, c: 3 });
const updatedMap = map.merge({ b: 20, d: 4 });
// { a: 1, b: 20, c: 3, d: 4 }

// 比较
const map1 = Map({ a: 1, b: 2 });
const map2 = Map({ a: 1, b: 2 });
console.log(map1 === map2); // false
console.log(is(map1, map2)); // true - 值相等
console.log(map1.equals(map2)); // true - 值相等

// 转换为普通 JS 对象
const plainObject = immutableUser.toJS();
const plainMap = immutableUser.toObject();
const plainList = immutableList.toArray();

在 React 中使用 Immutable

import React, { Component } from "react";
import { Map, List } from "immutable";

class TodoApp extends Component {
  constructor(props) {
    super(props);
    this.state = {
      // 使用 Immutable 数据结构
      data: Map({
        todos: List([
          Map({ id: 1, text: "学习 React", completed: false }),
          Map({ id: 2, text: "学习 Immutable", completed: false })
        ]),
        filter: "all"
      })
    };
  }

  addTodo = (text) => {
    const newTodo = Map({
      id: Date.now(),
      text,
      completed: false
    });

    this.setState(({ data }) => ({
      data: data.update("todos", (todos) => todos.push(newTodo))
    }));
  };

  toggleTodo = (id) => {
    this.setState(({ data }) => ({
      data: data.update("todos", (todos) =>
        todos.map((todo) =
          todo.get("id") === id
            ? todo.set("completed", !todo.get("completed"))
            : todo
        )
      )
    }));
  };

  setFilter = (filter) => {
    this.setState(({ data }) => ({
      data: data.set("filter", filter)
    }));
  };

  render() {
    const { data } = this.state;
    const todos = data.get("todos");
    const filter = data.get("filter");

    const filteredTodos = todos.filter((todo) => {
      if (filter === "active") return !todo.get("completed");
      if (filter === "completed") return todo.get("completed");
      return true;
    });

    return (
      <div>
        <div>
          {["all", "active", "completed"].map((f) => (
            <button
              key={f}
              onClick={() => this.setFilter(f)}
              style={{ fontWeight: filter === f ? "bold" : "normal" }}
            >
              {f}
            </button>
          ))}
        </div>
        <ul>
          {filteredTodos.map((todo) => (
            <li
              key={todo.get("id")}
              onClick={() => this.toggleTodo(todo.get("id"))}
              style={{
                textDecoration: todo.get("completed") ? "line-through" : "none"
              }}
            >
              {todo.get("text")}
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

使用 Immer 简化不可变更新

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

function TodoApp() {
  const [state, setState] = useState({
    users: [
      { id: 1, name: "张三", todos: [{ id: 1, text: "学习" }] },
      { id: 2, name: "李四", todos: [{ id: 2, text: "工作" }] }
    ],
    filter: "all"
  });

  // 使用 Immer 进行不可变更新
  const updateTodo = (userId, todoId, newText) => {
    setState(
      produce((draft) => {
        const user = draft.users.find((u) => u.id === userId);
        if (user) {
          const todo = user.todos.find((t) => t.id === todoId);
          if (todo) {
            todo.text = newText;
          }
        }
      })
    );
  };

  // 添加待办事项
  const addTodo = (userId, text) => {
    setState(
      produce((draft) => {
        const user = draft.users.find((u) => u.id === userId);
        if (user) {
          user.todos.push({
            id: Date.now(),
            text,
            completed: false
          });
        }
      })
    );
  };

  return <div>{/* ... */}</div>;
}

// 在 Redux 中使用 Immer
import produce from "immer";

const initialState = {
  todos: [],
  filter: "all"
};

const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case "ADD_TODO":
      return produce(state, (draft) => {
        draft.todos.push(action.payload);
      });

    case "TOGGLE_TODO":
      return produce(state, (draft) => {
        const todo = draft.todos.find((t) => t.id === action.payload);
        if (todo) {
          todo.completed = !todo.completed;
        }
      });

    case "SET_FILTER":
      return produce(state, (draft) => {
        draft.filter = action.payload;
      });

    default:
      return state;
  }
};

深入理解(原理剖析)

结构共享(Structural Sharing)

┌─────────────────────────────────────────────────────────────┐
│                   结构共享原理                               │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  原始树:                                                     │
│       A                                                      │
│      / \                                                     │
│     B   C                                                    │
│    / \   \                                                   │
│   D   E   F                                                  │
│                                                              │
│  修改 EE' 后:                                            │
│       A                                                      │
│      / \                                                     │
│     B   C      ← 共享                                        │
│    / \   \                                                   │
│   D   E'  FF 共享,D 共享,B 更新                      │
│       ↑                                                      │
│      新节点                                                  │
│                                                              │
│  只有从修改节点到根节点的路径需要创建新节点                  │
│  其他节点共享引用,节省内存                                  │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Trie 数据结构

// Immutable.js 内部使用 Trie 数据结构

// 例如 Vector Trie 用于实现 List
// Hash Array Mapped Trie (HAMT) 用于实现 Map

// 简化示意:
class VectorTrie {
  constructor(level = 0) {
    this.level = level;
    this.nodes = new Array(32); // 32 叉树
  }

  get(index) {
    const node = this.nodes[index >>> (this.level * 5) & 0x1f];
    if (this.level === 0) {
      return node;
    }
    return node.get(index);
  }

  set(index, value) {
    const newTrie = new VectorTrie(this.level);
    newTrie.nodes = [...this.nodes]; // 浅拷贝当前层

    const nodeIndex = index >>> (this.level * 5) & 0x1f;

    if (this.level === 0) {
      newTrie.nodes[nodeIndex] = value;
    } else {
      newTrie.nodes[nodeIndex] = this.nodes[nodeIndex]
        ? this.nodes[nodeIndex].set(index, value)
        : new VectorTrie(this.level - 1).set(index, value);
    }

    return newTrie;
  }
}

最佳实践

1. 在 React 中优化 shouldComponentUpdate

import { is } from "immutable";

class TodoItem extends React.Component {
  // 使用 Immutable 的 is 方法进行快速比较
  shouldComponentUpdate(nextProps) {
    return !is(this.props.todo, nextProps.todo);
  }

  render() {
    const { todo } = this.props;
    return <li>{todo.get("text")}</li>;
  }
}

// 或使用 PureComponent + Immutable
import { PureComponent } from "react";

class TodoList extends PureComponent {
  render() {
    const { todos } = this.props;
    return (
      <ul>
        {todos.map((todo) => (
          <TodoItem key={todo.get("id")} todo={todo} />
        ))}
      </ul>
    );
  }
}

2. 在 Redux 中使用 Immutable

// store.js
import { createStore } from "redux";
import { fromJS } from "immutable";
import rootReducer from "./reducers";

const initialState = fromJS({
  todos: [],
  filter: "all"
});

const store = createStore(rootReducer, initialState);

// reducer.js
import { fromJS, List, Map } from "immutable";

const initialState = fromJS({
  todos: [],
  filter: "all"
});

export default function todoReducer(state = initialState, action) {
  switch (action.type) {
    case "ADD_TODO":
      return state.update("todos", (todos) =>
        todos.push(Map({ ...action.payload, completed: false }))
      );

    case "TOGGLE_TODO":
      return state.update("todos", (todos) => {
        const index = todos.findIndex((t) => t.get("id") === action.payload);
        return todos.updateIn([index, "completed"], (completed) => !completed);
      });

    case "SET_FILTER":
      return state.set("filter", action.payload);

    default:
      return state;
  }
}

// 使用 redux-immutable 合并 reducer
import { combineReducers } from "redux-immutable";

const rootReducer = combineReducers({
  todos: todoReducer,
  user: userReducer
});

3. 避免过度使用 Immutable

// 不好的做法:所有数据都使用 Immutable
function OverkillExample() {
  const [name, setName] = useState(Map({ value: "" })); // 过度使用
  const [count, setCount] = useState(Map({ value: 0 })); // 过度使用

  return <div>{/* ... */}</div>;
}

// 好的做法:只在复杂数据结构使用
function AppropriateExample() {
  const [name, setName] = useState(""); // 简单值用普通 state
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState(List()); // 复杂结构用 Immutable

  return <div>{/* ... */}</div>;
}

4. 使用 Reselect 优化选择器

import { createSelector } from "reselect";
import { is } from "immutable";

// 基础选择器
const getTodos = (state) => state.get("todos");
const getFilter = (state) => state.get("filter");

// 派生选择器(带缓存)
export const getVisibleTodos = createSelector(
  [getTodos, getFilter],
  (todos, filter) => {
    switch (filter) {
      case "active":
        return todos.filter((t) => !t.get("completed"));
      case "completed":
        return todos.filter((t) => t.get("completed"));
      default:
        return todos;
    }
  }
);

// 使用 Immutable 的 is 进行相等性检查
export const getTodoStats = createSelector(
  [getTodos],
  (todos) => {
    const total = todos.size;
    const completed = todos.filter((t) => t.get("completed")).size;
    return Map({
      total,
      completed,
      active: total - completed
    });
  },
  {
    equalityCheck: is
  }
);

面试要点

  1. Immutable 是什么: 一旦创建就不能更改的数据,修改会返回新对象

  2. 核心优势:

    • 避免意外副作用
    • 快速比较(引用相等即可)
    • 时间旅行调试
    • 结构共享节省内存
  3. 实现方式:

    • 手动展开操作符(...)
    • Immutable.js
    • Immer
    • seamless-immutable
  4. 应用场景:

    • Redux state 管理
    • React 组件优化(shouldComponentUpdate)
    • 复杂数据结构更新
  5. 注意事项:

    • 简单数据不需要 Immutable
    • 与第三方库集成时注意转换
    • 学习成本较高