借用数组非破坏性方法来优化react的状态更新

背景

我们在写react项目中 使用到会更改原始数组的方法的时候,经常需要 使用[...arr]来拷贝一份,经常使用到的比如 sort reverse splice 等等,例如

js 复制代码
const handleSort = () => {
  // 先拷贝原数组,再排序(因为 sort() 会修改原数组)
  const newNumbers = [...numbers].sort((a, b) => a - b);
  setNumbers(newNumbers);
};

我们可以借助非破坏性方法来避免此类复制操作,比如 toSorted toReversed() toSpliced(start, deleteCount, ...items) with(index, value)

用法介绍

JavaScript 数组引入了几个新的方法,这些方法都是非破坏性 的,即不会修改原数组,而是返回一个新的修改后的数组。这与原有的 sort()reverse()splice() 等方法不同,后者会直接修改原数组。

  1. toSorted()

    • 功能:返回一个新的 sorted 数组,类似于 sort() 但不修改原数组
    • 参数:可选的比较函数
  2. toReversed()

    • 功能:返回一个新的 reversed 数组,类似于 reverse() 但不修改原数组
    • 无参数
  3. toSpliced(start, deleteCount, ...items)

    • 功能:返回一个新的 splice 后的数组,类似于 splice() 但不修改原数组
    • 参数:与 splice() 相同
  4. with(index, value)

    • 功能:返回一个在指定索引处更新了值的新数组
    • 参数:要更新的索引和新值

用法及输出

js 复制代码
// 原始数组
const numbers = [3, 1, 4, 2];
const fruits = ['apple', 'banana', 'cherry', 'date'];

// 1. toSorted() - 非破坏性排序
const sortedNumbers = numbers.toSorted();
console.log('原数组:', numbers); // [3, 1, 4, 2]
console.log('排序后:', sortedNumbers); // [1, 2, 3, 4]

// 使用比较函数
const sortedByLength = fruits.toSorted((a, b) => a.length - b.length);
console.log('按长度排序:', sortedByLength); // ['date', 'apple', 'banana', 'cherry']

// 2. toReversed() - 非破坏性反转
const reversedNumbers = numbers.toReversed();
console.log('原数组:', numbers); // [3, 1, 4, 2]
console.log('反转后:', reversedNumbers); // [2, 4, 1, 3]

// 3. toSpliced() - 非破坏性splice
// 从索引1开始删除1个元素,并插入"blueberry"
const modifiedFruits = fruits.toSpliced(1, 1, 'blueberry');
console.log('原数组:', fruits); // ['apple', 'banana', 'cherry', 'date']
console.log('修改后:', modifiedFruits); // ['apple', 'blueberry', 'cherry', 'date']

// 4. with(index, value) - 更新指定索引的值
const updatedNumbers = numbers.with(2, 10);
console.log('原数组:', numbers); // [3, 1, 4, 2]
console.log('更新后:', updatedNumbers); // [3, 1, 10, 2]

// 这些方法可以链式调用
const result = numbers
  .toSorted()          // [1, 2, 3, 4]
  .toReversed()        // [4, 3, 2, 1]
  .with(0, 5)          // [5, 3, 2, 1]
  .toSpliced(2, 0, 6); // [5, 3, 6, 2, 1]
  
console.log('链式调用结果:', result);

万能的 toSpliced

js 复制代码
const arr = [1, 2, 3];

// 模拟 push('末尾') → 在索引 3 处添加(超出原数组长度则自动加在末尾)
arr.toSpliced(arr.length, 0, '末尾'); // [1, 2, 3, '末尾']

// 模拟 unshift('开头') → 在索引 0 处添加
arr.toSpliced(0, 0, '开头'); // ['开头', 1, 2, 3]

// 模拟 splice(1, 1, '替换') → 从索引1删除1个元素,插入新值
arr.toSpliced(1, 1, '替换'); // [1, '替换', 3]

toSplicedwith 以及with能否替换 toSpliced

with 方法和 toSpliced 虽然都能修改数组元素,但它们的适用场景不同,不能完全用 with 替代 toSpliced 来模拟 splice(1, 1, '替换') 这种操作

具体区别:

with(index, value) 的核心作用是 「替换指定索引的元素」 (仅修改,不增删长度),而 splice(1, 1, '替换')「先删除元素,再插入新元素」(可能改变数组长度)。

举例说明:

假设原数组为 [1, 2, 3]

js 复制代码
const arr = [1, 2, 3];
const newArr = arr.toSpliced(1, 1, '替换'); 
// 结果:[1, '替换', 3]
// 过程:删除索引1的元素(2),插入'替换',数组长度不变(仍为3)
ini 复制代码
const arr = [1, 2, 3];
const newArr = arr.with(1, '替换'); 
// 结果:[1, '替换', 3]
// 过程:直接将索引1的元素(2)替换为'替换',数组长度不变

这两种方式在这个例子中结果相同,但本质不同:

  • toSpliced 是「删除 + 插入」的组合操作(先删后加)
  • with 只是单纯的「替换」操作(直接覆盖)
js 复制代码
const arr = [1, 2, 3, 4];

// 用 toSpliced 实现:删除索引1开始的2个元素,插入'x','y'
const newArr1 = arr.toSpliced(1, 2, 'x', 'y'); 
// 结果:[1, 'x', 'y', 4](长度不变)

// 用 toSpliced 实现:只删除不插入(长度减少)
const newArr2 = arr.toSpliced(1, 2); 
// 结果:[1, 4](长度从4变为2)

// 用 toSpliced 实现:只插入不删除(长度增加)
const newArr3 = arr.toSpliced(1, 0, 'x', 'y'); 
// 结果:[1, 'x', 'y', 2, 3, 4](长度从4变为6)

以上场景中,with 完全无法实现,因为它:

  • 只能修改单个索引的元素
  • 不能删除或插入元素(无法改变数组长度)
  • 一次只能处理一个元素

react更新变量时 原有的 sort()reverse()splice() 等方法为何需要拷贝

在 React 中,状态(state)是不可直接修改的(immutable)。当你需要更新数组状态时,必须创建一个新的数组实例,否则 React 可能无法检测到状态变化,导致组件不重新渲染。

过去,为了遵循这种不可变更新原则,开发者通常会使用扩展运算符(...)手动拷贝数组,再进行修改。而(toSortedtoReversedtoSplicedwith)本身就会返回新数组,完美契合 React 的状态更新需求,因此不再需要手动拷贝

原始写法

js 复制代码
const [numbers, setNumbers] = useState([3, 1, 4, 2]);

// 排序数组并更新状态
const handleSort = () => {
  // 先拷贝原数组,再排序(因为 sort() 会修改原数组)
  const newNumbers = [...numbers].sort((a, b) => a - b);
  setNumbers(newNumbers);
};

// 修改指定索引的值
const handleUpdate = () => {
  // 先拷贝原数组,再修改值
  const newNumbers = [...numbers];
  newNumbers[2] = 10;
  setNumbers(newNumbers);
};

新方法写法及原因

js 复制代码
const [numbers, setNumbers] = useState([3, 1, 4, 2]);

// 排序数组(toSorted 返回新数组,不修改原数组)
const handleSort = () => {
  setNumbers(numbers.toSorted((a, b) => a - b));
};

// 修改指定索引的值(with 直接返回新数组)
const handleUpdate = () => {
  setNumbers(numbers.with(2, 10));
};

// 反转数组(toReversed 返回新数组)
const handleReverse = () => {
  setNumbers(numbers.toReversed());
};
  • React 状态更新依赖「引用变化」来检测更新:只有当 setState 传入的新值与旧值引用不同时,组件才会重新渲染。
  • 旧方法(sortreversesplice)会修改原数组(引用不变),因此必须先用 ... 拷贝出一个新数组再操作。
  • 新方法(toSorted 等)天生返回新数组(引用不同),无需手动拷贝,直接传入 setState 即可触发更新。

错误示范

js 复制代码
const [numbers, setNumbers] = useState([3, 1, 4, 2]);

// 错误写法
const handleSort = () => {
  // 直接排序:sort() 会修改原数组,且返回原数组引用
  const newNumbers = numbers.sort((a, b) => a - b);
  
  // 此时 numbers 和 newNumbers 是同一个数组(引用相同)
  console.log(numbers === newNumbers); // true
  
  setNumbers(newNumbers); // React 会认为状态没变化,不重新渲染
};

执行后你会发现:

  • 原数组 numbers 已经被修改(违反不可变性)
  • 组件可能不会重新渲染(因为引用没变)

结尾

愿借助 toSorted toReversed() toSpliced(start, deleteCount, ...items) with(index, value) 实现更少的代码 更好的划水

相关推荐
朱程22 分钟前
AI 编程时代手工匠人代码打造 React 项目实战(四):使用路由参数 & mock 接口数据
前端
PineappleCoder25 分钟前
深入浅出React状态提升:告别组件间的"鸡同鸭讲"!
前端·react.js
wycode35 分钟前
Vue2源码笔记(1)编译时-模板代码如何生效之生成AST树
前端·vue.js
程序员嘉逸1 小时前
LESS 预处理器
前端
橡皮擦1991 小时前
PanJiaChen /vue-element-admin 多标签页TagsView方案总结
前端
程序员嘉逸1 小时前
SASS/SCSS 预处理器
前端
咕噜分发企业签名APP加固彭于晏1 小时前
腾讯云eo激活码领取
前端·面试
子林super1 小时前
MySQL 复制延迟的排查思路
前端
CondorHero1 小时前
轻松覆盖 Element-Plus 禁用按钮样式
前端
源猿人1 小时前
nginx代理如何配置和如何踩到坑篇
前端·nginx