collectionHandlers.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. import { toRaw, reactive, readonly, ReactiveFlags } from './reactive'
  2. import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
  3. import { TrackOpTypes, TriggerOpTypes } from './operations'
  4. import {
  5. isObject,
  6. capitalize,
  7. hasOwn,
  8. hasChanged,
  9. toRawType,
  10. isMap
  11. } from '@vue/shared'
  12. export type CollectionTypes = IterableCollections | WeakCollections
  13. type IterableCollections = Map<any, any> | Set<any>
  14. type WeakCollections = WeakMap<any, any> | WeakSet<any>
  15. type MapTypes = Map<any, any> | WeakMap<any, any>
  16. type SetTypes = Set<any> | WeakSet<any>
  17. const toReactive = <T extends unknown>(value: T): T =>
  18. isObject(value) ? reactive(value) : value
  19. const toReadonly = <T extends unknown>(value: T): T =>
  20. isObject(value) ? readonly(value as Record<any, any>) : value
  21. const toShallow = <T extends unknown>(value: T): T => value
  22. const getProto = <T extends CollectionTypes>(v: T): any =>
  23. Reflect.getPrototypeOf(v)
  24. function get(
  25. target: MapTypes,
  26. key: unknown,
  27. isReadonly = false,
  28. isShallow = false
  29. ) {
  30. // #1772: readonly(reactive(Map)) should return readonly + reactive version
  31. // of the value
  32. target = (target as any)[ReactiveFlags.RAW]
  33. const rawTarget = toRaw(target)
  34. const rawKey = toRaw(key)
  35. if (key !== rawKey) {
  36. !isReadonly && track(rawTarget, TrackOpTypes.GET, key)
  37. }
  38. !isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
  39. const { has } = getProto(rawTarget)
  40. const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
  41. if (has.call(rawTarget, key)) {
  42. return wrap(target.get(key))
  43. } else if (has.call(rawTarget, rawKey)) {
  44. return wrap(target.get(rawKey))
  45. }
  46. }
  47. function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
  48. const target = (this as any)[ReactiveFlags.RAW]
  49. const rawTarget = toRaw(target)
  50. const rawKey = toRaw(key)
  51. if (key !== rawKey) {
  52. !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  53. }
  54. !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
  55. return key === rawKey
  56. ? target.has(key)
  57. : target.has(key) || target.has(rawKey)
  58. }
  59. function size(target: IterableCollections, isReadonly = false) {
  60. target = (target as any)[ReactiveFlags.RAW]
  61. !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  62. return Reflect.get(target, 'size', target)
  63. }
  64. function add(this: SetTypes, value: unknown) {
  65. value = toRaw(value)
  66. const target = toRaw(this)
  67. const proto = getProto(target)
  68. const hadKey = proto.has.call(target, value)
  69. target.add(value)
  70. if (!hadKey) {
  71. trigger(target, TriggerOpTypes.ADD, value, value)
  72. }
  73. return this
  74. }
  75. function set(this: MapTypes, key: unknown, value: unknown) {
  76. value = toRaw(value)
  77. const target = toRaw(this)
  78. const { has, get } = getProto(target)
  79. let hadKey = has.call(target, key)
  80. if (!hadKey) {
  81. key = toRaw(key)
  82. hadKey = has.call(target, key)
  83. } else if (__DEV__) {
  84. checkIdentityKeys(target, has, key)
  85. }
  86. const oldValue = get.call(target, key)
  87. target.set(key, value)
  88. if (!hadKey) {
  89. trigger(target, TriggerOpTypes.ADD, key, value)
  90. } else if (hasChanged(value, oldValue)) {
  91. trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  92. }
  93. return this
  94. }
  95. function deleteEntry(this: CollectionTypes, key: unknown) {
  96. const target = toRaw(this)
  97. const { has, get } = getProto(target)
  98. let hadKey = has.call(target, key)
  99. if (!hadKey) {
  100. key = toRaw(key)
  101. hadKey = has.call(target, key)
  102. } else if (__DEV__) {
  103. checkIdentityKeys(target, has, key)
  104. }
  105. const oldValue = get ? get.call(target, key) : undefined
  106. // forward the operation before queueing reactions
  107. const result = target.delete(key)
  108. if (hadKey) {
  109. trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  110. }
  111. return result
  112. }
  113. function clear(this: IterableCollections) {
  114. const target = toRaw(this)
  115. const hadItems = target.size !== 0
  116. const oldTarget = __DEV__
  117. ? isMap(target)
  118. ? new Map(target)
  119. : new Set(target)
  120. : undefined
  121. // forward the operation before queueing reactions
  122. const result = target.clear()
  123. if (hadItems) {
  124. trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  125. }
  126. return result
  127. }
  128. function createForEach(isReadonly: boolean, isShallow: boolean) {
  129. return function forEach(
  130. this: IterableCollections,
  131. callback: Function,
  132. thisArg?: unknown
  133. ) {
  134. const observed = this as any
  135. const target = observed[ReactiveFlags.RAW]
  136. const rawTarget = toRaw(target)
  137. const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
  138. !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
  139. return target.forEach((value: unknown, key: unknown) => {
  140. // important: make sure the callback is
  141. // 1. invoked with the reactive map as `this` and 3rd arg
  142. // 2. the value received should be a corresponding reactive/readonly.
  143. return callback.call(thisArg, wrap(value), wrap(key), observed)
  144. })
  145. }
  146. }
  147. interface Iterable {
  148. [Symbol.iterator](): Iterator
  149. }
  150. interface Iterator {
  151. next(value?: any): IterationResult
  152. }
  153. interface IterationResult {
  154. value: any
  155. done: boolean
  156. }
  157. function createIterableMethod(
  158. method: string | symbol,
  159. isReadonly: boolean,
  160. isShallow: boolean
  161. ) {
  162. return function(
  163. this: IterableCollections,
  164. ...args: unknown[]
  165. ): Iterable & Iterator {
  166. const target = (this as any)[ReactiveFlags.RAW]
  167. const rawTarget = toRaw(target)
  168. const targetIsMap = isMap(rawTarget)
  169. const isPair =
  170. method === 'entries' || (method === Symbol.iterator && targetIsMap)
  171. const isKeyOnly = method === 'keys' && targetIsMap
  172. const innerIterator = target[method](...args)
  173. const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
  174. !isReadonly &&
  175. track(
  176. rawTarget,
  177. TrackOpTypes.ITERATE,
  178. isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
  179. )
  180. // return a wrapped iterator which returns observed versions of the
  181. // values emitted from the real iterator
  182. return {
  183. // iterator protocol
  184. next() {
  185. const { value, done } = innerIterator.next()
  186. return done
  187. ? { value, done }
  188. : {
  189. value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
  190. done
  191. }
  192. },
  193. // iterable protocol
  194. [Symbol.iterator]() {
  195. return this
  196. }
  197. }
  198. }
  199. }
  200. function createReadonlyMethod(type: TriggerOpTypes): Function {
  201. return function(this: CollectionTypes, ...args: unknown[]) {
  202. if (__DEV__) {
  203. const key = args[0] ? `on key "${args[0]}" ` : ``
  204. console.warn(
  205. `${capitalize(type)} operation ${key}failed: target is readonly.`,
  206. toRaw(this)
  207. )
  208. }
  209. return type === TriggerOpTypes.DELETE ? false : this
  210. }
  211. }
  212. const mutableInstrumentations: Record<string, Function> = {
  213. get(this: MapTypes, key: unknown) {
  214. return get(this, key)
  215. },
  216. get size() {
  217. return size((this as unknown) as IterableCollections)
  218. },
  219. has,
  220. add,
  221. set,
  222. delete: deleteEntry,
  223. clear,
  224. forEach: createForEach(false, false)
  225. }
  226. const shallowInstrumentations: Record<string, Function> = {
  227. get(this: MapTypes, key: unknown) {
  228. return get(this, key, false, true)
  229. },
  230. get size() {
  231. return size((this as unknown) as IterableCollections)
  232. },
  233. has,
  234. add,
  235. set,
  236. delete: deleteEntry,
  237. clear,
  238. forEach: createForEach(false, true)
  239. }
  240. const readonlyInstrumentations: Record<string, Function> = {
  241. get(this: MapTypes, key: unknown) {
  242. return get(this, key, true)
  243. },
  244. get size() {
  245. return size((this as unknown) as IterableCollections, true)
  246. },
  247. has(this: MapTypes, key: unknown) {
  248. return has.call(this, key, true)
  249. },
  250. add: createReadonlyMethod(TriggerOpTypes.ADD),
  251. set: createReadonlyMethod(TriggerOpTypes.SET),
  252. delete: createReadonlyMethod(TriggerOpTypes.DELETE),
  253. clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
  254. forEach: createForEach(true, false)
  255. }
  256. const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
  257. iteratorMethods.forEach(method => {
  258. mutableInstrumentations[method as string] = createIterableMethod(
  259. method,
  260. false,
  261. false
  262. )
  263. readonlyInstrumentations[method as string] = createIterableMethod(
  264. method,
  265. true,
  266. false
  267. )
  268. shallowInstrumentations[method as string] = createIterableMethod(
  269. method,
  270. false,
  271. true
  272. )
  273. })
  274. function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  275. const instrumentations = shallow
  276. ? shallowInstrumentations
  277. : isReadonly
  278. ? readonlyInstrumentations
  279. : mutableInstrumentations
  280. return (
  281. target: CollectionTypes,
  282. key: string | symbol,
  283. receiver: CollectionTypes
  284. ) => {
  285. if (key === ReactiveFlags.IS_REACTIVE) {
  286. return !isReadonly
  287. } else if (key === ReactiveFlags.IS_READONLY) {
  288. return isReadonly
  289. } else if (key === ReactiveFlags.RAW) {
  290. return target
  291. }
  292. return Reflect.get(
  293. hasOwn(instrumentations, key) && key in target
  294. ? instrumentations
  295. : target,
  296. key,
  297. receiver
  298. )
  299. }
  300. }
  301. export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  302. get: createInstrumentationGetter(false, false)
  303. }
  304. export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
  305. get: createInstrumentationGetter(false, true)
  306. }
  307. export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
  308. get: createInstrumentationGetter(true, false)
  309. }
  310. function checkIdentityKeys(
  311. target: CollectionTypes,
  312. has: (key: unknown) => boolean,
  313. key: unknown
  314. ) {
  315. const rawKey = toRaw(key)
  316. if (rawKey !== key && has.call(target, rawKey)) {
  317. const type = toRawType(target)
  318. console.warn(
  319. `Reactive ${type} contains both the raw and reactive ` +
  320. `versions of the same object${type === `Map` ? ` as keys` : ``}, ` +
  321. `which can lead to inconsistencies. ` +
  322. `Avoid differentiating between the raw and reactive versions ` +
  323. `of an object and only use the reactive version if possible.`
  324. )
  325. }
  326. }