collectionHandlers.ts 10.0 KB

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