借用数组非破坏性方法来优化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) 实现更少的代码 更好的划水

相关推荐
非凡ghost6 分钟前
U盘歌单管理器(适用于车载音乐U盘) 中文绿色版
前端·windows·学习·计算机外设·软件需求
打工人小夏12 分钟前
前端面试题
前端·css·前端框架·vue·html·es6·html5
laplace012315 分钟前
Part 4. LangChain 1.0 Agent 开发流程(Markdown 笔记)
前端·javascript·笔记·python·语言模型·langchain
Aliex_git15 分钟前
性能优化 - 渲染优化
前端·javascript·笔记·学习·性能优化·html
Jenlybein18 分钟前
Git 仓库过滤敏感信息,通过配置 clean/smudge 过滤器的方式
前端·后端·github
千寻girling30 分钟前
面试官 : “ 说一下 Vue 的 8 个生命周期钩子都做了什么 ? ”
前端·vue.js·面试
Heo31 分钟前
Vue3 应用实例创建及页面渲染底层原理
前端·javascript·面试
C_心欲无痕35 分钟前
nodejs - express:流行的 Web 应用框架
前端·node.js·express
sophie旭36 分钟前
webpack异步加载原理梳理解构
前端·javascript·webpack
小小荧37 分钟前
Vue 原生渲染真要来了?Lynx引擎首次跑通Vue
前端·javascript