跳到主要内容

数组方法插件

概述

数组方法插件(enableArrayMethods())通过避免在迭代期间创建不必要的 Proxy,优化了 Immer producer 中的数组操作。这可以显著提升大量使用数组的操作的性能。

为什么这一点很重要? 如果不使用该插件,迭代期间每次访问数组元素(例如在 filterfindslice 中)都会创建一个 Proxy 对象。对于包含 1000 个元素的数组,仅一次迭代就意味着 1000 次以上的 Proxy trap 调用。启用插件后,回调会接收基础值(未经 Proxy 包装的值),只有在追踪修改需要时才会创建 Proxy。

安装

在应用的入口处启用一次插件:

import {enableArrayMethods} from "immer"

enableArrayMethods()

这会使你的打包体积增加约 2KB

修改数组的方法

这些方法会就地修改数组,直接操作 draft 的内部副本,而不会为每个元素创建 Proxy:

方法返回值说明
push()新长度在末尾添加元素
pop()被移除的元素移除并返回最后一个元素
shift()被移除的元素移除并返回第一个元素
unshift()新长度在开头添加元素
splice()被移除的元素在任意位置添加或移除元素
sort()draft 数组就地对元素排序
reverse()draft 数组就地反转数组
import {produce, enableArrayMethods} from "immer"

enableArrayMethods()

const base = {items: [3, 1, 4, 1, 5]}

const result = produce(base, draft => {
draft.items.push(9) // 在末尾添加 9
draft.items.sort() // 排序:[1, 1, 3, 4, 5, 9]
draft.items.reverse() // 反转:[9, 5, 4, 3, 1, 1]
})

不修改数组的方法

不修改数组的方法可以根据返回值分为以下几类:

子集操作(返回 draft)

这些方法选择原数组中已有的元素,并为返回的元素创建 draft Proxy。回调接收的是基础值(这正是优化所在),但返回的数组中包含新创建的 draft Proxy,它们仍指向原来的位置。修改返回的元素会影响 draft 状态。

方法返回值是否为 draft?
filter()匹配元素组成的数组✅ 是
slice()指定范围内的元素组成的数组✅ 是
find()第一个匹配元素或 undefined✅ 是
findLast()最后一个匹配元素或 undefined✅ 是
const base = {
items: [
{id: 1, value: 10},
{id: 2, value: 20},
{id: 3, value: 30}
]
}

const result = produce(base, draft => {
// filter 返回 draft,修改会追踪到原数组
const filtered = draft.items.filter(item => item.value > 15)
filtered[0].value = 999 // 这会影响 draft.items[1]

// find 返回一个 draft,修改会被追踪
const found = draft.items.find(item => item.id === 3)
if (found) {
found.value = 888 // 这会影响 draft.items[2]
}

// slice 返回 draft
const sliced = draft.items.slice(0, 2)
sliced[0].value = 777 // 这会影响 draft.items[0]
})

console.log(result.items[0].value) // 777
console.log(result.items[1].value) // 999
console.log(result.items[2].value) // 888

转换操作(返回基础值)

这些方法会创建可能包含外部元素或经过重新组织的数据的新数组。它们返回的是基础值,而不是 draft。修改返回的元素不会追踪回 draft 状态。

方法返回值是否为 draft?
concat()合并后的新数组❌ 否
flat()扁平化后的新数组❌ 否
const base = {items: [{id: 1, value: 10}]}

const result = produce(base, draft => {
// concat 返回基础值,修改不会被追踪
const concatenated = draft.items.concat([{id: 2, value: 20}])
concatenated[0].value = 999 // 这不会影响 draft.items[0]

// 如需真正使用 concat 的结果,请将其赋值:
draft.items = draft.items.concat([{id: 2, value: 20}])
})

// 原值未改变,因为 concat 的结果没有被赋值给它
console.log(result.items[0].value) // 10(未改变)

为什么要这样区分?

  • 子集操作filterslicefind)选择原数组中已经存在的元素。返回 draft 可以让修改传播回数据源。
  • 转换操作concatflat)创建可能包含外部元素或经过重新组织的数据的新数据结构,因此无法进行实用的 draft 追踪。

返回原始值的方法

这些方法返回原始值(数字、布尔值、字符串)。原始值不能成为 draft,因此不存在追踪问题:

方法返回值
indexOf()数字(索引或 -1)
lastIndexOf()数字(索引或 -1)
includes()布尔值
some()布尔值
every()布尔值
findIndex()数字(索引或 -1)
findLastIndex()数字(索引或 -1)
join()字符串
toString()字符串
toLocaleString()字符串
const base = {
items: [
{id: 1, active: true},
{id: 2, active: false}
]
}

const result = produce(base, draft => {
const index = draft.items.findIndex(item => item.id === 2)
const hasActive = draft.items.some(item => item.active)
const allActive = draft.items.every(item => item.active)

console.log(index) // 1
console.log(hasActive) // true
console.log(allActive) // false
})

未被重写的方法

以下方法不会被插件拦截,而是继续按照标准 Proxy 行为工作。回调接收 draft,修改会正常追踪:

方法说明
map()转换每个元素
flatMap()映射后再扁平化
forEach()对每个元素执行回调
reduce()归并为单个值
reduceRight()从右向左归并为单个值
const base = {
items: [
{id: 1, value: 10, nested: {count: 0}},
{id: 2, value: 20, nested: {count: 0}}
]
}

const result = produce(base, draft => {
// forEach 接收 draft,修改会正常工作
draft.items.forEach(item => {
item.value *= 2
})

// map 未被重写,回调接收 draft
// 返回数组中的元素也是从 draft.items 中提取的 draft
const mapped = draft.items.map(item => item.nested)
// 对结果数组中元素的修改会传播回去
mapped[0].count = 999 // ✅ 这会影响 draft.items[0].nested.count
})

console.log(result.items[0].nested.count) // 999

回调行为

对于被重写的方法,回调接收的是基础值(不是 draft)。这是优化的核心,因为它避免了在迭代期间为每个元素创建 Proxy。

const base = {
items: [
{id: 1, value: 10},
{id: 2, value: 20}
]
}

produce(base, draft => {
draft.items.filter(item => {
// 这里的 item 是基础值,而不是 draft
// 读取属性没有问题
return item.value > 15

// 但这里的直接修改不会被追踪:
// item.value = 999 // ❌ 不会影响 draft
})

// 应当改用返回的 draft:
const filtered = draft.items.filter(item => item.value > 15)
filtered[0].value = 999 // ✅ 可以生效,因为 filtered[0] 是 draft
})

方法返回行为汇总

类别方法返回值是否追踪修改?
子集filterslicefindfindLastdraft Proxy✅ 是
转换concatflat基础值❌ 否
原始值indexOfincludessomeeveryfindIndexfindLastIndexlastIndexOfjointoStringtoLocaleString原始值不适用
修改数组pushpopshiftunshiftsplicesortreverse取决于方法✅ 是(修改 draft)
未重写mapflatMapforEachreducereduceRight标准行为✅ 是(回调接收 draft)

何时使用

在以下情况下,可以启用数组方法插件:

  • 应用的 producer 中包含大量数组迭代
  • 经常对大型数组使用 filterfindsomeevery 等方法
  • 性能分析显示数组操作是性能瓶颈

该插件在以下场景中最有帮助:

  • 大型数组(100 个以上元素)
  • 频繁调用包含数组操作的 producer
  • 大多数元素不会被修改的读取密集型操作(筛选、搜索)

性能收益

不使用插件时:

  • 迭代期间每次访问数组元素都会创建一个 Proxy
  • 对 1000 个元素执行一次 filter(),会创建 1000 个以上的 Proxy

使用插件时:

  • 回调直接接收基础值
  • 只为你实际修改的特定元素或符合筛选条件的元素创建 Proxy
// 不使用插件:约 3000 次以上的 Proxy trap 调用
// 使用插件:约 10~20 次 Proxy trap 调用
const result = produce(largeState, draft => {
const filtered = draft.items.filter(x => x.value > threshold)
// 只有被修改的元素才会创建 Proxy
filtered.forEach(item => {
item.processed = true
})
})