arrayInstrumentations.ts 9.5 KB

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