数组状态与集合操作
数组状态与集合操作 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
数组是 React 状态里最常见的结构,而 JavaScript 数组方法一半复制、一半原地改,混着用就出诡异 bug。这一课把增、删、改、排序四个操作的不可变写法一次配齐,并给出「哪些方法碰不得」的明确清单。
问题场景
任务管理器要四个操作:新增、编辑、删除、上移下移。第一版代码混着写:
function promote(task) {
tasks.splice(index, 1); // 原地移除
tasks.unshift(task); // 原地插入
setTasks(tasks); // 同一引用,React 不渲染
}
更隐蔽的是另一版:tasks.sort() 返回值是同一个数组(原地排序),写 setTasks(tasks.sort()) 看似有「新值」,实际引用没变,界面照样不动。
核心解释
判断一个数组方法能不能用在 React 状态上,只看一条:它返回新数组,还是改原数组? 改原数组(变异方法)一律不碰,改用复制版:
| 操作 | 变异方法(禁用) | 不可变替代 |
|---|---|---|
| 新增 | push / unshift |
[...tasks, item] / [item, ...tasks] |
| 删除 | splice |
filter / slice 拼接 |
| 替换 | 直接赋值 tasks[i] = x |
map 命中替换 |
| 排序 | sort / reverse |
toSorted / toReversed / [...tasks].sort() |
| 抽取 | splice |
slice |
四个核心操作的惯用写法:
// 新增:展开 + 新对象(id 在创建时生成)
setTasks([...tasks, { id: newId(), title, done: false }]);
// 编辑:map 遍历,命中的新建对象,未命中的保持引用
setTasks(tasks.map((t) => (t.id === id ? { ...t, title: nextTitle } : t)));
// 删除:filter 天然返回新数组
setTasks(tasks.filter((t) => t.id !== id));
// 上移:slice 拼出交换后的新数组
setTasks([
...tasks.slice(0, i - 1),
tasks[i],
tasks[i - 1],
...tasks.slice(i + 1),
]);
上移那段值得读三遍:slice(0, i-1) 是前段、两个元素交换、slice(i+1) 是后段,全部拼进一个新数组。原数组从头到尾没被碰过。对象元素的处理原则与 R12 一致:整个数组必然新建,但没被修改的对象保持原引用,不强行深拷贝。
toSorted / toReversed / with 是 ES2023 加入的复制版方法,语义与变异版完全相同,新项目可以直接用;考虑旧环境兼容时用 [...tasks].sort()。
示例拆解
实验区是一个完整任务板,配一个「数据快照」面板,关键点如下:
- 四类操作按钮齐全:新增(带输入)、编辑(改标题)、删除、上移/下移,全部走不可变路径;
- 快照面板实时显示当前数组的 JSON 和「数组身份变化次数」——每次操作身份必变,未被编辑的任务对象身份不变;
- 对照按钮「危险示范」:执行
tasks.push/tasks.sort()后再setTasks(tasks),点了没反应,正好复现问题场景; - 排序按钮演示
toSorted:随机打乱后重新按名称排正。
验证方式:每点一次操作按钮,快照面板的「身份变化」+1 且数据正确更新;危险示范按钮点了身份不变、界面不动。
动手任务
打开本课实验区,完成三个小改动:
- 加「批量删除已完成」:
filter一次删掉所有done的任务。 - 加「移到末尾」按钮,用
filter+ 展开组合实现,不碰splice。 - 把危险示范改成正确写法(先
[...tasks]复制再排序),让按钮生效。
常见错误
- push/splice/sort/reverse 直接用于状态数组:原地改 + 同引用 setState,React 不渲染;换成复制版方法。
- 删除时用下标:
filter((t, i) => i !== idx)在并发操作下会删错行;按业务 id 删,和渲染的 key 保持同一身份依据。 - 每次更新都深拷贝整个数组:
JSON.parse(JSON.stringify(tasks))丢方法、丢 undefined,还白白破坏所有对象引用;只需新建被改动的部分。 - 排序时忘了去变异:
tasks.sort((a, b) => ...)原地生效,即使随后 setState 新引用也已经是排过的,会把「未排序状态」从历史里抹掉。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能背出「变异方法禁用清单」并说出每个的复制版替代
- 增删改排序四类操作都能当场写出不可变版本
- 更新后数组是新引用,未修改的对象保持原引用
参考资料
- React 官方文档:更新数组中的状态 —— 每种数组操作的不变性写法
- MDN:Array.prototype.toSorted —— 复制版排序方法的说明
读完这一课了?
标记完成后,工作台会安排接下来的学习。