同对象一样,当你想要更新存储于 state 中的数组时,你需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。
在没有 mutation 的前提下更新数组
每次要更新一个数组时,你需要把一个新的数组传入 state 的 setting 方法中。为此,你可以通过使用像 filter()
和 map()
这样不会直接修改原始值的方法,从原始数组生成一个新的数组。然后你就可以将 state 设置为这个新生成的数组。
避免使用 (会改变原始数组) | 推荐使用 (会返回一个新数组) | |
---|---|---|
添加元素 | push,unshift | concat,[...arr] 展开语法 |
删除元素 | pop,shift,splice | filter,slice |
替换元素 | splice,arr[i] = ... 赋值 | map |
排序 | reverse,sort | 先将数组复制一份 |
或者,你可以使用
Immer
,这样你便可以使用表格中的所有方法了。
向数组中添加元素
应该创建一个 新 数组,其包含了原始数组的所有元素 以及 一个在末尾的新元素。
最简单的一种就是使用 ... 数组展开 语法
setArtists( // 替换 state
[ // 是通过传入一个新数组实现的
...artists, // 新数组包含原数组的所有元素
{ id: nextId++, name: name } // 并在末尾添加了一个新的元素
]
);
数组展开运算符还允许你把新添加的元素放在原始的 ...artists 之前
setArtists([
{ id: nextId++, name: name },
...artists // 将原数组中的元素放在末尾
]);
...
数组展开就可以完成 push() 和 unshift() 的工作,将新元素添加到数组的末尾和开头。
从数组中删除元素
setArtists(
artists.filter(a => a.id !== artist.id)
);
filter
并不会改变原始数组
转换数组
如果你想改变数组中的某些或全部元素,你可以用 map() 创建一个新数组。你传入 map 的函数决定了要根据每个元素的值或索引(或二者都要)对元素做何处理。
替换数组中的元素
要替换一个元素,请使用 map 创建一个新数组。在你的 map 回调里,第二个参数是元素的索引。使用索引来判断最终是返回原始的元素(即回调的第一个参数)还是替换成其他值:
向数组中插入元素
有时,你也许想向数组特定位置插入一个元素,这个位置既不在数组开头,也不在末尾。为此,你可以将数组展开运算符 ...
和 slice()
方法一起使用。slice()
方法让你从数组中切出“一片”。为了将元素插入数组,你需要先展开原数组在插入点之前的切片,然后插入新元素,最后展开原数组中剩下的部分。
function handleClick() {
const insertAt = 1; // 可能是任何索引
const nextArtists = [
// 插入点之前的元素:
...artists.slice(0, insertAt),
// 新的元素:
{ id: nextId++, name: name },
// 插入点之后的元素:
...artists.slice(insertAt)
];
setArtists(nextArtists);
setName('');
}
其他改变数组的情况
总会有一些事,是你仅仅依靠展开运算符和 map() 或者 filter() 等不会直接修改原值的方法所无法做到的。例如,你可能想翻转数组,或是对数组排序。而 JavaScript 中的 reverse() 和 sort() 方法会改变原数组,所以你无法直接使用它们。
然而,你可以先拷贝这个数组,再改变这个拷贝后的值。
function handleClick() {
const nextList = [...list];
nextList.reverse();
setList(nextList);
}
然而,即使你拷贝了数组,你还是不能直接修改其内部的元素。这是因为数组的拷贝是浅拷贝——新的数组中依然保留了与原始数组相同的元素。因此,如果你修改了拷贝数组内部的某个对象,其实你正在直接修改当前的 state。举个例子,像下面的代码就会带来问题。
const nextList = [...list];
nextList[0].seen = true; // 问题:直接修改了 list[0] 的值
setList(nextList);
虽然 nextList
和 list
是两个不同的数组,nextList[0]
和 list[0]
却指向了同一个对象。