Skip to content

Commit fc221cc

Browse files
committed
Document array method internal behaviors
1 parent a76f3f2 commit fc221cc

1 file changed

Lines changed: 128 additions & 10 deletions

File tree

src/plugins/arrayMethods.ts

Lines changed: 128 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ import {
77
ProxyArrayState
88
} from "../internal"
99

10-
// Type-safe union of mutating array method names
10+
/**
11+
* Methods that directly modify the array in place.
12+
* These operate on the copy without creating per-element proxies:
13+
* - `push`, `pop`: Add/remove from end
14+
* - `shift`, `unshift`: Add/remove from start (marks all indices reassigned)
15+
* - `splice`: Add/remove at arbitrary position (marks all indices reassigned)
16+
* - `reverse`, `sort`: Reorder elements (marks all indices reassigned)
17+
*/
1118
type MutatingArrayMethod =
1219
| "push"
1320
| "pop"
@@ -17,7 +24,22 @@ type MutatingArrayMethod =
1724
| "reverse"
1825
| "sort"
1926

20-
// Type-safe union of non-mutating array method names
27+
/**
28+
* Methods that read from the array without modifying it.
29+
* These fall into distinct categories based on return semantics:
30+
*
31+
* **Subset operations** (return drafts - mutations propagate):
32+
* - `filter`, `slice`: Return array of draft proxies
33+
* - `find`, `findLast`: Return single draft proxy or undefined
34+
*
35+
* **Transform operations** (return base values - mutations don't track):
36+
* - `concat`, `flat`: Create new structures, not subsets of original
37+
*
38+
* **Primitive-returning** (no draft needed):
39+
* - `findIndex`, `findLastIndex`, `indexOf`, `lastIndexOf`: Return numbers
40+
* - `some`, `every`, `includes`: Return booleans
41+
* - `join`, `toString`, `toLocaleString`: Return strings
42+
*/
2143
type NonMutatingArrayMethod =
2244
| "filter"
2345
| "slice"
@@ -29,18 +51,51 @@ type NonMutatingArrayMethod =
2951
| "findLastIndex"
3052
| "some"
3153
| "every"
32-
| "reduce"
33-
| "reduceRight"
3454
| "indexOf"
3555
| "lastIndexOf"
3656
| "includes"
3757
| "join"
3858
| "toString"
3959
| "toLocaleString"
4060

41-
// Union of all array operation methods
61+
/** Union of all array operation methods handled by the plugin. */
4262
export type ArrayOperationMethod = MutatingArrayMethod | NonMutatingArrayMethod
4363

64+
/**
65+
* Enables optimized array method handling for Immer drafts.
66+
*
67+
* This plugin overrides array methods to avoid unnecessary Proxy creation during iteration,
68+
* significantly improving performance for array-heavy operations.
69+
*
70+
* **Mutating methods** (push, pop, shift, unshift, splice, sort, reverse):
71+
* Operate directly on the copy without creating per-element proxies.
72+
*
73+
* **Non-mutating methods** fall into categories:
74+
* - **Subset operations** (filter, slice, find, findLast): Return draft proxies - mutations track
75+
* - **Transform operations** (concat, flat): Return base values - mutations don't track
76+
* - **Primitive-returning** (indexOf, includes, some, every, etc.): Return primitives
77+
*
78+
* **Important**: Callbacks for overridden methods receive base values, not drafts.
79+
* This is the core performance optimization.
80+
*
81+
* @example
82+
* ```ts
83+
* import { enableArrayMethods, produce } from "immer"
84+
*
85+
* enableArrayMethods()
86+
*
87+
* const next = produce(state, draft => {
88+
* // Optimized - no proxy creation per element
89+
* draft.items.sort((a, b) => a.value - b.value)
90+
*
91+
* // filter returns drafts - mutations propagate
92+
* const filtered = draft.items.filter(x => x.value > 5)
93+
* filtered[0].value = 999 // Affects draft.items[originalIndex]
94+
* })
95+
* ```
96+
*
97+
* @see https://immerjs.github.io/immer/array-methods
98+
*/
4499
export function enableArrayMethods() {
45100
const SHIFTING_METHODS = new Set<MutatingArrayMethod>(["shift", "unshift"])
46101

@@ -134,7 +189,15 @@ export function enableArrayMethods() {
134189
return Math.min(index, length)
135190
}
136191

137-
// Consolidated handler functions
192+
/**
193+
* Handles mutating operations that add/remove elements (push, pop, shift, unshift, splice).
194+
*
195+
* Operates directly on `state.copy_` without creating per-element proxies.
196+
* For shifting methods (shift, unshift), marks all indices as reassigned since
197+
* indices shift.
198+
*
199+
* @returns For push/pop/shift/unshift: the native method result. For others: the draft.
200+
*/
138201
function handleSimpleOperation(
139202
state: ProxyArrayState,
140203
method: string,
@@ -155,6 +218,15 @@ export function enableArrayMethods() {
155218
})
156219
}
157220

221+
/**
222+
* Handles reordering operations (reverse, sort) that change element order.
223+
*
224+
* Operates directly on `state.copy_` and marks all indices as reassigned
225+
* since element positions change. Does not mark length as changed since
226+
* these operations preserve array length.
227+
*
228+
* @returns The draft proxy for method chaining.
229+
*/
158230
function handleReorderingOperation(
159231
state: ProxyArrayState,
160232
method: string,
@@ -171,12 +243,29 @@ export function enableArrayMethods() {
171243
) // Don't mark length as changed
172244
}
173245

246+
/**
247+
* Creates an interceptor function for a specific array method.
248+
*
249+
* The interceptor wraps array method calls to:
250+
* 1. Set `state.operationMethod` flag during execution (allows proxy `get` trap
251+
* to detect we're inside an optimized method and skip proxy creation)
252+
* 2. Route to appropriate handler based on method type
253+
* 3. Clean up the operation flag in `finally` block
254+
*
255+
* The `operationMethod` flag is the key mechanism that enables the proxy's `get`
256+
* trap to return base values instead of creating nested proxies during iteration.
257+
*
258+
* @param state - The proxy array state
259+
* @param originalMethod - Name of the array method being intercepted
260+
* @returns Interceptor function that handles the method call
261+
*/
174262
function createMethodInterceptor(
175263
state: ProxyArrayState,
176264
originalMethod: string
177265
) {
178266
return function interceptedMethod(...args: any[]) {
179-
// Enter operation mode
267+
// Enter operation mode - this flag tells the proxy's get trap to return
268+
// base values instead of creating nested proxies during iteration
180269
const method = originalMethod as ArrayOperationMethod
181270
enterOperation(state, method)
182271

@@ -203,12 +292,38 @@ export function enableArrayMethods() {
203292
return handleNonMutatingOperation(state, method, args)
204293
}
205294
} finally {
206-
// Always exit operation mode
295+
// Always exit operation mode - must be in finally to handle exceptions
207296
exitOperation(state)
208297
}
209298
}
210299
}
211300

301+
/**
302+
* Handles non-mutating array methods with different return semantics.
303+
*
304+
* **Subset operations** return draft proxies for mutation tracking:
305+
* - `filter`, `slice`: Return `state.draft_[i]` for each selected element
306+
* - `find`, `findLast`: Return `state.draft_[i]` for the found element
307+
*
308+
* This allows mutations on returned elements to propagate back to the draft:
309+
* ```ts
310+
* const filtered = draft.items.filter(x => x.value > 5)
311+
* filtered[0].value = 999 // Mutates draft.items[originalIndex]
312+
* ```
313+
*
314+
* **Transform operations** return base values (no draft tracking):
315+
* - `concat`, `flat`: These create NEW arrays rather than selecting subsets.
316+
* Since the result structure differs from the original, tracking mutations
317+
* back to specific draft indices would be impractical/impossible.
318+
*
319+
* **Primitive operations** return the native result directly:
320+
* - `indexOf`, `includes`, `some`, `every`, `join`, etc.
321+
*
322+
* @param state - The proxy array state
323+
* @param method - The non-mutating method name
324+
* @param args - Arguments passed to the method
325+
* @returns Drafts for subset operations, base values for transforms, primitives otherwise
326+
*/
212327
function handleNonMutatingOperation(
213328
state: ProxyArrayState,
214329
method: NonMutatingArrayMethod,
@@ -264,8 +379,11 @@ export function enableArrayMethods() {
264379
return result
265380
}
266381

267-
// For other methods (indexOf, includes, join, etc.), call on base array
268-
// These don't need drafts since they don't expose items for mutation
382+
// For other methods, call on base array directly:
383+
// - indexOf, includes, join, toString: Return primitives, no draft needed
384+
// - concat, flat: Return NEW arrays (not subsets). Elements are base values.
385+
// This is intentional - concat/flat create new data structures rather than
386+
// selecting subsets of the original, making draft tracking impractical.
269387
return source[method as keyof typeof Array.prototype](...args)
270388
}
271389

0 commit comments

Comments
 (0)