-
-
Notifications
You must be signed in to change notification settings - Fork 8.4k
/
arrayInstrumentations.ts
328 lines (285 loc) · 9.03 KB
/
arrayInstrumentations.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
import { TrackOpTypes } from './constants'
import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
import { isProxy, isShallow, toRaw, toReactive } from './reactive'
import { ARRAY_ITERATE_KEY, track } from './dep'
import { isArray } from '@vue/shared'
/**
* Track array iteration and return:
* - if input is reactive: a cloned raw array with reactive values
* - if input is non-reactive or shallowReactive: the original raw array
*/
export function reactiveReadArray<T>(array: T[]): T[] {
const raw = toRaw(array)
if (raw === array) return raw
track(raw, TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY)
return isShallow(array) ? raw : raw.map(toReactive)
}
/**
* Track array iteration and return raw array
*/
export function shallowReadArray<T>(arr: T[]): T[] {
track((arr = toRaw(arr)), TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY)
return arr
}
export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
__proto__: null,
[Symbol.iterator]() {
return iterator(this, Symbol.iterator, toReactive)
},
concat(...args: unknown[]) {
return reactiveReadArray(this).concat(
...args.map(x => (isArray(x) ? reactiveReadArray(x) : x)),
)
},
entries() {
return iterator(this, 'entries', (value: [number, unknown]) => {
value[1] = toReactive(value[1])
return value
})
},
every(
fn: (item: unknown, index: number, array: unknown[]) => unknown,
thisArg?: unknown,
) {
return apply(this, 'every', fn, thisArg, undefined, arguments)
},
filter(
fn: (item: unknown, index: number, array: unknown[]) => unknown,
thisArg?: unknown,
) {
return apply(this, 'filter', fn, thisArg, v => v.map(toReactive), arguments)
},
find(
fn: (item: unknown, index: number, array: unknown[]) => boolean,
thisArg?: unknown,
) {
return apply(this, 'find', fn, thisArg, toReactive, arguments)
},
findIndex(
fn: (item: unknown, index: number, array: unknown[]) => boolean,
thisArg?: unknown,
) {
return apply(this, 'findIndex', fn, thisArg, undefined, arguments)
},
findLast(
fn: (item: unknown, index: number, array: unknown[]) => boolean,
thisArg?: unknown,
) {
return apply(this, 'findLast', fn, thisArg, toReactive, arguments)
},
findLastIndex(
fn: (item: unknown, index: number, array: unknown[]) => boolean,
thisArg?: unknown,
) {
return apply(this, 'findLastIndex', fn, thisArg, undefined, arguments)
},
// flat, flatMap could benefit from ARRAY_ITERATE but are not straight-forward to implement
forEach(
fn: (item: unknown, index: number, array: unknown[]) => unknown,
thisArg?: unknown,
) {
return apply(this, 'forEach', fn, thisArg, undefined, arguments)
},
includes(...args: unknown[]) {
return searchProxy(this, 'includes', args)
},
indexOf(...args: unknown[]) {
return searchProxy(this, 'indexOf', args)
},
join(separator?: string) {
return reactiveReadArray(this).join(separator)
},
// keys() iterator only reads `length`, no optimisation required
lastIndexOf(...args: unknown[]) {
return searchProxy(this, 'lastIndexOf', args)
},
map(
fn: (item: unknown, index: number, array: unknown[]) => unknown,
thisArg?: unknown,
) {
return apply(this, 'map', fn, thisArg, undefined, arguments)
},
pop() {
return noTracking(this, 'pop')
},
push(...args: unknown[]) {
return noTracking(this, 'push', args)
},
reduce(
fn: (
acc: unknown,
item: unknown,
index: number,
array: unknown[],
) => unknown,
...args: unknown[]
) {
return reduce(this, 'reduce', fn, args)
},
reduceRight(
fn: (
acc: unknown,
item: unknown,
index: number,
array: unknown[],
) => unknown,
...args: unknown[]
) {
return reduce(this, 'reduceRight', fn, args)
},
shift() {
return noTracking(this, 'shift')
},
// slice could use ARRAY_ITERATE but also seems to beg for range tracking
some(
fn: (item: unknown, index: number, array: unknown[]) => unknown,
thisArg?: unknown,
) {
return apply(this, 'some', fn, thisArg, undefined, arguments)
},
splice(...args: unknown[]) {
return noTracking(this, 'splice', args)
},
toReversed() {
// @ts-expect-error user code may run in es2016+
return reactiveReadArray(this).toReversed()
},
toSorted(comparer?: (a: unknown, b: unknown) => number) {
// @ts-expect-error user code may run in es2016+
return reactiveReadArray(this).toSorted(comparer)
},
toSpliced(...args: unknown[]) {
// @ts-expect-error user code may run in es2016+
return (reactiveReadArray(this).toSpliced as any)(...args)
},
unshift(...args: unknown[]) {
return noTracking(this, 'unshift', args)
},
values() {
return iterator(this, 'values', toReactive)
},
}
// instrument iterators to take ARRAY_ITERATE dependency
function iterator(
self: unknown[],
method: keyof Array<unknown>,
wrapValue: (value: any) => unknown,
) {
// note that taking ARRAY_ITERATE dependency here is not strictly equivalent
// to calling iterate on the proxified array.
// creating the iterator does not access any array property:
// it is only when .next() is called that length and indexes are accessed.
// pushed to the extreme, an iterator could be created in one effect scope,
// partially iterated in another, then iterated more in yet another.
// given that JS iterator can only be read once, this doesn't seem like
// a plausible use-case, so this tracking simplification seems ok.
const arr = shallowReadArray(self)
const iter = (arr[method] as any)() as IterableIterator<unknown> & {
_next: IterableIterator<unknown>['next']
}
if (arr !== self && !isShallow(self)) {
iter._next = iter.next
iter.next = () => {
const result = iter._next()
if (result.value) {
result.value = wrapValue(result.value)
}
return result
}
}
return iter
}
// in the codebase we enforce es2016, but user code may run in environments
// higher than that
type ArrayMethods = keyof Array<any> | 'findLast' | 'findLastIndex'
const arrayProto = Array.prototype
// instrument functions that read (potentially) all items
// to take ARRAY_ITERATE dependency
function apply(
self: unknown[],
method: ArrayMethods,
fn: (item: unknown, index: number, array: unknown[]) => unknown,
thisArg?: unknown,
wrappedRetFn?: (result: any) => unknown,
args?: IArguments,
) {
const arr = shallowReadArray(self)
const needsWrap = arr !== self && !isShallow(self)
// @ts-expect-error our code is limited to es2016 but user code is not
const methodFn = arr[method]
// #11759
// If the method being called is from a user-extended Array, the arguments will be unknown
// (unknown order and unknown parameter types). In this case, we skip the shallowReadArray
// handling and directly call apply with self.
if (methodFn !== arrayProto[method as any]) {
const result = methodFn.apply(self, args)
return needsWrap ? toReactive(result) : result
}
let wrappedFn = fn
if (arr !== self) {
if (needsWrap) {
wrappedFn = function (this: unknown, item, index) {
return fn.call(this, toReactive(item), index, self)
}
} else if (fn.length > 2) {
wrappedFn = function (this: unknown, item, index) {
return fn.call(this, item, index, self)
}
}
}
const result = methodFn.call(arr, wrappedFn, thisArg)
return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result
}
// instrument reduce and reduceRight to take ARRAY_ITERATE dependency
function reduce(
self: unknown[],
method: keyof Array<any>,
fn: (acc: unknown, item: unknown, index: number, array: unknown[]) => unknown,
args: unknown[],
) {
const arr = shallowReadArray(self)
let wrappedFn = fn
if (arr !== self) {
if (!isShallow(self)) {
wrappedFn = function (this: unknown, acc, item, index) {
return fn.call(this, acc, toReactive(item), index, self)
}
} else if (fn.length > 3) {
wrappedFn = function (this: unknown, acc, item, index) {
return fn.call(this, acc, item, index, self)
}
}
}
return (arr[method] as any)(wrappedFn, ...args)
}
// instrument identity-sensitive methods to account for reactive proxies
function searchProxy(
self: unknown[],
method: keyof Array<any>,
args: unknown[],
) {
const arr = toRaw(self) as any
track(arr, TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY)
// we run the method using the original args first (which may be reactive)
const res = arr[method](...args)
// if that didn't work, run it again using raw values.
if ((res === -1 || res === false) && isProxy(args[0])) {
args[0] = toRaw(args[0])
return arr[method](...args)
}
return res
}
// instrument length-altering mutation methods to avoid length being tracked
// which leads to infinite loops in some cases (#2137)
function noTracking(
self: unknown[],
method: keyof Array<any>,
args: unknown[] = [],
) {
pauseTracking()
startBatch()
const res = (toRaw(self) as any)[method].apply(self, args)
endBatch()
resetTracking()
return res
}