说说 React 的 immutable 的理解?应用场景?
问题解析(面试官考察点)
面试官通过此问题主要考察:
- 对不可变数据概念的理解
- 对 JavaScript 中实现不可变数据的方式的了解
- 对 Immutable.js 等库的使用
- 对不可变数据在 React 中应用场景的认识
- 对不可变数据带来的性能优化的理解
核心概念(基础知识点)
什么是 Immutable
Immutable(不可变)是指一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。
Immutable 的核心特性
- 不可变性: 数据创建后不能被修改
- 持久化数据结构: 新数据尽可能复用旧数据的结构
- 结构共享: 只修改变化的部分,未变化的部分共享引用
为什么需要 Immutable
JavaScript 中的对象和数组是引用类型,直接修改会带来以下问题:
- 意外的副作用: 修改一个对象可能影响其他地方
- 比较困难: 需要深度比较才能判断数据是否变化
- 不可预测性: 难以追踪数据的变化来源
详细解答(代码示例)
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 │
│ │
│ 修改 E 为 E' 后: │
│ A │
│ / \ │
│ B C ← 共享 │
│ / \ \ │
│ D E' F ← F 共享,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
}
);
面试要点
-
Immutable 是什么: 一旦创建就不能更改的数据,修改会返回新对象
-
核心优势:
- 避免意外副作用
- 快速比较(引用相等即可)
- 时间旅行调试
- 结构共享节省内存
-
实现方式:
- 手动展开操作符(...)
- Immutable.js
- Immer
- seamless-immutable
-
应用场景:
- Redux state 管理
- React 组件优化(shouldComponentUpdate)
- 复杂数据结构更新
-
注意事项:
- 简单数据不需要 Immutable
- 与第三方库集成时注意转换
- 学习成本较高