Skip to content

Commit 2f0ea7f

Browse files
authored
docs: add zh-CN array methods translation (#1285)
Co-authored-by: lijiayou0728 <277368198+lijiayou0728@users.noreply.github.com>
1 parent 9daf3cd commit 2f0ea7f

1 file changed

Lines changed: 277 additions & 0 deletions

File tree

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
---
2+
id: array-methods
3+
title: 数组方法插件
4+
---
5+
6+
<center>
7+
<div data-ea-publisher="immerjs" data-ea-type="image" className="horizontal bordered"></div>
8+
</center>
9+
10+
## 概述
11+
12+
数组方法插件(`enableArrayMethods()`)通过避免在迭代期间创建不必要的 Proxy,优化了 Immer producer 中的数组操作。这可以显著提升大量使用数组的操作的性能。
13+
14+
**为什么这一点很重要?** 如果不使用该插件,迭代期间每次访问数组元素(例如在 `filter``find``slice` 中)都会创建一个 Proxy 对象。对于包含 1000 个元素的数组,仅一次迭代就意味着 1000 次以上的 Proxy trap 调用。启用插件后,回调会接收基础值(未经 Proxy 包装的值),只有在追踪修改需要时才会创建 Proxy。
15+
16+
## 安装
17+
18+
在应用的入口处启用一次插件:
19+
20+
```javascript
21+
import {enableArrayMethods} from "immer"
22+
23+
enableArrayMethods()
24+
```
25+
26+
这会使你的打包体积增加约 **2KB**
27+
28+
## 修改数组的方法
29+
30+
这些方法会就地修改数组,直接操作 draft 的内部副本,而不会为每个元素创建 Proxy:
31+
32+
| 方法 | 返回值 | 说明 |
33+
| ----------- | ------------ | ---------------------------- |
34+
| `push()` | 新长度 | 在末尾添加元素 |
35+
| `pop()` | 被移除的元素 | 移除并返回最后一个元素 |
36+
| `shift()` | 被移除的元素 | 移除并返回第一个元素 |
37+
| `unshift()` | 新长度 | 在开头添加元素 |
38+
| `splice()` | 被移除的元素 | 在任意位置添加或移除元素 |
39+
| `sort()` | draft 数组 | 就地对元素排序 |
40+
| `reverse()` | draft 数组 | 就地反转数组 |
41+
42+
```javascript
43+
import {produce, enableArrayMethods} from "immer"
44+
45+
enableArrayMethods()
46+
47+
const base = {items: [3, 1, 4, 1, 5]}
48+
49+
const result = produce(base, draft => {
50+
draft.items.push(9) // 在末尾添加 9
51+
draft.items.sort() // 排序:[1, 1, 3, 4, 5, 9]
52+
draft.items.reverse() // 反转:[9, 5, 4, 3, 1, 1]
53+
})
54+
```
55+
56+
## 不修改数组的方法
57+
58+
不修改数组的方法可以根据返回值分为以下几类:
59+
60+
### 子集操作(返回 draft)
61+
62+
这些方法选择原数组中已有的元素,并为返回的元素**创建 draft Proxy**。回调接收的是**基础值**(这正是优化所在),但**返回的数组**中包含新创建的 draft Proxy,它们仍指向原来的位置。**修改返回的元素会影响 draft 状态。**
63+
64+
| 方法 | 返回值 | 是否为 draft? |
65+
| ------------ | ------------------------------ | -------------- |
66+
| `filter()` | 匹配元素组成的数组 | ✅ 是 |
67+
| `slice()` | 指定范围内的元素组成的数组 | ✅ 是 |
68+
| `find()` | 第一个匹配元素或 `undefined` | ✅ 是 |
69+
| `findLast()` | 最后一个匹配元素或 `undefined` | ✅ 是 |
70+
71+
```javascript
72+
const base = {
73+
items: [
74+
{id: 1, value: 10},
75+
{id: 2, value: 20},
76+
{id: 3, value: 30}
77+
]
78+
}
79+
80+
const result = produce(base, draft => {
81+
// filter 返回 draft,修改会追踪到原数组
82+
const filtered = draft.items.filter(item => item.value > 15)
83+
filtered[0].value = 999 // 这会影响 draft.items[1]
84+
85+
// find 返回一个 draft,修改会被追踪
86+
const found = draft.items.find(item => item.id === 3)
87+
if (found) {
88+
found.value = 888 // 这会影响 draft.items[2]
89+
}
90+
91+
// slice 返回 draft
92+
const sliced = draft.items.slice(0, 2)
93+
sliced[0].value = 777 // 这会影响 draft.items[0]
94+
})
95+
96+
console.log(result.items[0].value) // 777
97+
console.log(result.items[1].value) // 999
98+
console.log(result.items[2].value) // 888
99+
```
100+
101+
### 转换操作(返回基础值)
102+
103+
这些方法会创建可能包含外部元素或经过重新组织的数据的**新数组**。它们返回的是**基础值**,而不是 draft。**修改返回的元素不会追踪回 draft 状态。**
104+
105+
| 方法 | 返回值 | 是否为 draft? |
106+
| ---------- | ------------------ | -------------- |
107+
| `concat()` | 合并后的新数组 | ❌ 否 |
108+
| `flat()` | 扁平化后的新数组 | ❌ 否 |
109+
110+
```javascript
111+
const base = {items: [{id: 1, value: 10}]}
112+
113+
const result = produce(base, draft => {
114+
// concat 返回基础值,修改不会被追踪
115+
const concatenated = draft.items.concat([{id: 2, value: 20}])
116+
concatenated[0].value = 999 // 这不会影响 draft.items[0]
117+
118+
// 如需真正使用 concat 的结果,请将其赋值:
119+
draft.items = draft.items.concat([{id: 2, value: 20}])
120+
})
121+
122+
// 原值未改变,因为 concat 的结果没有被赋值给它
123+
console.log(result.items[0].value) // 10(未改变)
124+
```
125+
126+
**为什么要这样区分?**
127+
128+
- **子集操作**`filter``slice``find`)选择原数组中已经存在的元素。返回 draft 可以让修改传播回数据源。
129+
- **转换操作**`concat``flat`)创建可能包含外部元素或经过重新组织的数据的新数据结构,因此无法进行实用的 draft 追踪。
130+
131+
### 返回原始值的方法
132+
133+
这些方法返回原始值(数字、布尔值、字符串)。原始值不能成为 draft,因此不存在追踪问题:
134+
135+
| 方法 | 返回值 |
136+
| ------------------ | -------------------- |
137+
| `indexOf()` | 数字(索引或 -1) |
138+
| `lastIndexOf()` | 数字(索引或 -1) |
139+
| `includes()` | 布尔值 |
140+
| `some()` | 布尔值 |
141+
| `every()` | 布尔值 |
142+
| `findIndex()` | 数字(索引或 -1) |
143+
| `findLastIndex()` | 数字(索引或 -1) |
144+
| `join()` | 字符串 |
145+
| `toString()` | 字符串 |
146+
| `toLocaleString()` | 字符串 |
147+
148+
```javascript
149+
const base = {
150+
items: [
151+
{id: 1, active: true},
152+
{id: 2, active: false}
153+
]
154+
}
155+
156+
const result = produce(base, draft => {
157+
const index = draft.items.findIndex(item => item.id === 2)
158+
const hasActive = draft.items.some(item => item.active)
159+
const allActive = draft.items.every(item => item.active)
160+
161+
console.log(index) // 1
162+
console.log(hasActive) // true
163+
console.log(allActive) // false
164+
})
165+
```
166+
167+
## 未被重写的方法
168+
169+
以下方法**不会**被插件拦截,而是继续按照标准 Proxy 行为工作。回调接收 draft,修改会正常追踪:
170+
171+
| 方法 | 说明 |
172+
| --------------- | ---------------------- |
173+
| `map()` | 转换每个元素 |
174+
| `flatMap()` | 映射后再扁平化 |
175+
| `forEach()` | 对每个元素执行回调 |
176+
| `reduce()` | 归并为单个值 |
177+
| `reduceRight()` | 从右向左归并为单个值 |
178+
179+
```javascript
180+
const base = {
181+
items: [
182+
{id: 1, value: 10, nested: {count: 0}},
183+
{id: 2, value: 20, nested: {count: 0}}
184+
]
185+
}
186+
187+
const result = produce(base, draft => {
188+
// forEach 接收 draft,修改会正常工作
189+
draft.items.forEach(item => {
190+
item.value *= 2
191+
})
192+
193+
// map 未被重写,回调接收 draft
194+
// 返回数组中的元素也是从 draft.items 中提取的 draft
195+
const mapped = draft.items.map(item => item.nested)
196+
// 对结果数组中元素的修改会传播回去
197+
mapped[0].count = 999 // ✅ 这会影响 draft.items[0].nested.count
198+
})
199+
200+
console.log(result.items[0].nested.count) // 999
201+
```
202+
203+
## 回调行为
204+
205+
对于被重写的方法,回调接收的是**基础值**(不是 draft)。这是优化的核心,因为它避免了在迭代期间为每个元素创建 Proxy。
206+
207+
```javascript
208+
const base = {
209+
items: [
210+
{id: 1, value: 10},
211+
{id: 2, value: 20}
212+
]
213+
}
214+
215+
produce(base, draft => {
216+
draft.items.filter(item => {
217+
// 这里的 item 是基础值,而不是 draft
218+
// 读取属性没有问题
219+
return item.value > 15
220+
221+
// 但这里的直接修改不会被追踪:
222+
// item.value = 999 // ❌ 不会影响 draft
223+
})
224+
225+
// 应当改用返回的 draft:
226+
const filtered = draft.items.filter(item => item.value > 15)
227+
filtered[0].value = 999 // ✅ 可以生效,因为 filtered[0] 是 draft
228+
})
229+
```
230+
231+
## 方法返回行为汇总
232+
233+
| 类别 | 方法 | 返回值 | 是否追踪修改? |
234+
| ------------ | -------------------------------------------------------------------------------------------------- | ------------ | ----------------------- |
235+
| **子集** | `filter``slice``find``findLast` | draft Proxy | ✅ 是 |
236+
| **转换** | `concat``flat` | 基础值 | ❌ 否 |
237+
| **原始值** | `indexOf``includes``some``every``findIndex``findLastIndex``lastIndexOf``join``toString``toLocaleString` | 原始值 | 不适用 |
238+
| **修改数组** | `push``pop``shift``unshift``splice``sort``reverse` | 取决于方法 | ✅ 是(修改 draft) |
239+
| **未重写** | `map``flatMap``forEach``reduce``reduceRight` | 标准行为 | ✅ 是(回调接收 draft) |
240+
241+
## 何时使用
242+
243+
在以下情况下,可以启用数组方法插件:
244+
245+
- 应用的 producer 中包含大量数组迭代
246+
- 经常对大型数组使用 `filter``find``some``every` 等方法
247+
- 性能分析显示数组操作是性能瓶颈
248+
249+
该插件在以下场景中最有帮助:
250+
251+
- 大型数组(100 个以上元素)
252+
- 频繁调用包含数组操作的 producer
253+
- 大多数元素不会被修改的读取密集型操作(筛选、搜索)
254+
255+
## 性能收益
256+
257+
**不使用插件时:**
258+
259+
- 迭代期间每次访问数组元素都会创建一个 Proxy
260+
- 对 1000 个元素执行一次 `filter()`,会创建 1000 个以上的 Proxy
261+
262+
**使用插件时:**
263+
264+
- 回调直接接收基础值
265+
- 只为你实际修改的特定元素或符合筛选条件的元素创建 Proxy
266+
267+
```javascript
268+
// 不使用插件:约 3000 次以上的 Proxy trap 调用
269+
// 使用插件:约 10~20 次 Proxy trap 调用
270+
const result = produce(largeState, draft => {
271+
const filtered = draft.items.filter(x => x.value > threshold)
272+
// 只有被修改的元素才会创建 Proxy
273+
filtered.forEach(item => {
274+
item.processed = true
275+
})
276+
})
277+
```

0 commit comments

Comments
 (0)