跳至主内容区

数组方法插件

[非官方测试版翻译]

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

概述

数组方法插件 (enableArrayMethods()) 通过避免迭代过程中不必要的 Proxy 创建,优化了 Immer producer 中的数组操作。对于数组密集型操作可带来显著的性能提升。

这为何重要? 未启用插件时,迭代过程中的每次数组元素访问(例如在 filterfindslice 中)都会创建 Proxy 对象。对于含 1000 个元素的数组,仅迭代就会触发 1000+ 次代理陷阱调用。启用插件后,回调函数接收基础值(非代理值),且仅在需要变更追踪时才创建代理。

安装

在应用入口点一次性启用插件:

import {enableArrayMethods} from "immer"

enableArrayMethods()

这将增加约 2KB 的打包体积。

变更型方法

以下方法直接修改原数组,操作时直接作用于草稿的内部副本而无需创建逐元素代理:

MethodReturnsDescription
push()New lengthAdds elements to the end
pop()Removed elementRemoves and returns the last element
shift()Removed elementRemoves and returns the first element
unshift()New lengthAdds elements to the beginning
splice()Removed elementsAdds/removes elements at any position
sort()The draft arraySorts elements in place
reverse()The draft arrayReverses the array in place
import {produce, enableArrayMethods} from "immer"

enableArrayMethods()

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

const result = produce(base, draft => {
draft.items.push(9) // Adds 9 to end
draft.items.sort() // Sorts: [1, 1, 3, 4, 5, 9]
draft.items.reverse() // Reverses: [9, 5, 4, 3, 1, 1]
})

非变更型方法

非变更型方法根据返回值分为三类:

子集操作(返回草稿)

这些方法筛选原数组中存在的项,并为返回项创建草稿代理。回调接收基础值(优化点),但返回的数组包含指向原始位置的新建草稿代理。对返回项的修改将影响草稿状态。

MethodReturnsDrafts?
filter()Array of matching items✅ Yes
slice()Array of items in range✅ Yes
find()First matching item or undefined✅ Yes
findLast()Last matching item or undefined✅ Yes
const base = {
items: [
{id: 1, value: 10},
{id: 2, value: 20},
{id: 3, value: 30}
]
}

const result = produce(base, draft => {
// filter returns drafts - mutations track back to original
const filtered = draft.items.filter(item => item.value > 15)
filtered[0].value = 999 // This WILL affect draft.items[1]

// find returns a draft - mutations track back
const found = draft.items.find(item => item.id === 3)
if (found) {
found.value = 888 // This WILL affect draft.items[2]
}

// slice returns drafts
const sliced = draft.items.slice(0, 2)
sliced[0].value = 777 // This WILL affect draft.items[0]
})

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

转换操作(返回基础值)

这些方法创建新数组(可能包含外部项或重构数据),返回基础值而非草稿。对返回项的修改不会追溯至草稿状态。

MethodReturnsDrafts?
concat()New combined array❌ No
flat()New flattened array❌ No
const base = {items: [{id: 1, value: 10}]}

const result = produce(base, draft => {
// concat returns base values - mutations DON'T track
const concatenated = draft.items.concat([{id: 2, value: 20}])
concatenated[0].value = 999 // This will NOT affect draft.items[0]

// To actually use concat results, assign them:
draft.items = draft.items.concat([{id: 2, value: 20}])
})

// Original unchanged because concat result wasn't assigned
console.log(result.items[0].value) // 10 (unchanged)

为何区分处理?

  • 子集操作 (filter, slice, find) 筛选原数组中存在的项。返回草稿可使修改传递回源数据

  • 转换操作 (concat, flat) 创建可能包含外部项或重构数据的新数据结构,使草稿追踪不可行

返回基础类型的方法

以下方法返回基础类型值(数字、布尔值、字符串)。由于基础类型不可草稿化,不存在追踪问题:

MethodReturns
indexOf()Number (index or -1)
lastIndexOf()Number (index or -1)
includes()Boolean
some()Boolean
every()Boolean
findIndex()Number (index or -1)
findLastIndex()Number (index or -1)
join()String
toString()String
toLocaleString()String
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 机制运作。回调接收草稿,变更追踪正常进行:

MethodDescription
map()Transform each element
flatMap()Map then flatten
forEach()Execute callback for each element
reduce()Reduce to single value
reduceRight()Reduce from right to left
const base = {
items: [
{id: 1, value: 10, nested: {count: 0}},
{id: 2, value: 20, nested: {count: 0}}
]
}

const result = produce(base, draft => {
// forEach receives drafts - mutations work normally
draft.items.forEach(item => {
item.value *= 2
})

// map is NOT overridden - callbacks receive drafts
// The returned array items are also drafts (extracted from draft.items)
const mapped = draft.items.map(item => item.nested)
// Mutations to the result array propagate back
mapped[0].count = 999 // ✅ This affects draft.items[0].nested.count
})

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

回调行为

对于覆盖的方法,回调接收基础值(非草稿)。这是核心优化——避免在迭代期间为每个元素创建代理。

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

produce(base, draft => {
draft.items.filter(item => {
// `item` is a base value here, NOT a draft
// Reading properties works fine
return item.value > 15

// But direct mutation here won't be tracked:
// item.value = 999 // ❌ Won't affect draft
})

// Instead, use the returned draft:
const filtered = draft.items.filter(item => item.value > 15)
filtered[0].value = 999 // ✅ This works because filtered[0] is a draft
})

方法返回行为摘要

CategoryMethodsReturnsMutations Track?
Subsetfilter, slice, find, findLastDraft proxies✅ Yes
Transformconcat, flatBase values❌ No
PrimitiveindexOf, includes, some, every, findIndex, findLastIndex, lastIndexOf, join, toString, toLocaleStringPrimitivesN/A
Mutatingpush, pop, shift, unshift, splice, sort, reverseVarious✅ Yes (modifies draft)
Not Overriddenmap, flatMap, forEach, reduce, reduceRightStandard behavior✅ Yes (callbacks get drafts)

使用场景

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

  • 应用在 producer 中有大量数组迭代操作

  • 频繁对大数组使用 filter, find, some, every 等方法

  • 性能分析显示数组操作是瓶颈

该插件在以下场景收益最大:

  • 大型数组(100+ 元素)

  • 频繁调用含数组操作的 producer

  • 读密集型操作(筛选、搜索)且多数元素未被修改

性能收益

未启用插件时:

  • 迭代期间每次数组元素访问都创建 Proxy

  • 对 1000 元素执行 filter() = 1000+ 次代理创建

启用插件后:

  • 回调直接接收基础值

  • 仅在实际发生变动的特定元素或匹配过滤条件的元素上创建代理(Proxies)

// Without plugin: ~3000+ proxy trap invocations
// With plugin: ~10-20 proxy trap invocations
const result = produce(largeState, draft => {
const filtered = draft.items.filter(x => x.value > threshold)
// Only items you mutate get proxied
filtered.forEach(item => {
item.processed = true
})
})