collectionHandlers.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import { toRaw, reactive, readonly } from './reactive'
  2. import { track, trigger, ITERATE_KEY } from './effect'
  3. import { TrackOpTypes, TriggerOpTypes } from './operations'
  4. import { LOCKED } from './lock'
  5. import { isObject, capitalize, hasOwn, hasChanged } from '@vue/shared'
  6. export type CollectionTypes = IterableCollections | WeakCollections
  7. type IterableCollections = Map<any, any> | Set<any>
  8. type WeakCollections = WeakMap<any, any> | WeakSet<any>
  9. type MapTypes = Map<any, any> | WeakMap<any, any>
  10. type SetTypes = Set<any> | WeakSet<any>
  11. const toReactive = <T extends unknown>(value: T): T =>
  12. isObject(value) ? reactive(value) : value
  13. const toReadonly = <T extends unknown>(value: T): T =>
  14. isObject(value) ? readonly(value) : value
  15. const getProto = <T extends CollectionTypes>(v: T): any =>
  16. Reflect.getPrototypeOf(v)
  17. function get(
  18. target: MapTypes,
  19. key: unknown,
  20. wrap: typeof toReactive | typeof toReadonly
  21. ) {
  22. target = toRaw(target)
  23. const rawKey = toRaw(key)
  24. track(target, TrackOpTypes.GET, rawKey)
  25. const { has, get } = getProto(target)
  26. if (has.call(target, key)) {
  27. return wrap(get.call(target, key))
  28. } else if (has.call(target, rawKey)) {
  29. return wrap(get.call(target, rawKey))
  30. }
  31. }
  32. function has(this: CollectionTypes, key: unknown): boolean {
  33. const target = toRaw(this)
  34. const rawKey = toRaw(key)
  35. track(target, TrackOpTypes.HAS, rawKey)
  36. const has = getProto(target).has
  37. return has.call(target, key) || has.call(target, rawKey)
  38. }
  39. function size(target: IterableCollections) {
  40. target = toRaw(target)
  41. track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  42. return Reflect.get(getProto(target), 'size', target)
  43. }
  44. function add(this: SetTypes, value: unknown) {
  45. value = toRaw(value)
  46. const target = toRaw(this)
  47. const proto = getProto(target)
  48. const hadKey = proto.has.call(target, value)
  49. const result = proto.add.call(target, value)
  50. if (!hadKey) {
  51. trigger(target, TriggerOpTypes.ADD, value, value)
  52. }
  53. return result
  54. }
  55. function set(this: MapTypes, key: unknown, value: unknown) {
  56. value = toRaw(value)
  57. key = toRaw(key)
  58. const target = toRaw(this)
  59. const proto = getProto(target)
  60. const hadKey = proto.has.call(target, key)
  61. const oldValue = proto.get.call(target, key)
  62. const result = proto.set.call(target, key, value)
  63. if (!hadKey) {
  64. trigger(target, TriggerOpTypes.ADD, key, value)
  65. } else if (hasChanged(value, oldValue)) {
  66. trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  67. }
  68. return result
  69. }
  70. function deleteEntry(this: CollectionTypes, key: unknown) {
  71. const target = toRaw(this)
  72. const { has, get, delete: del } = getProto(target)
  73. let hadKey = has.call(target, key)
  74. if (!hadKey) {
  75. key = toRaw(key)
  76. hadKey = has.call(target, key)
  77. }
  78. const oldValue = get ? get.call(target, key) : undefined
  79. // forward the operation before queueing reactions
  80. const result = del.call(target, key)
  81. if (hadKey) {
  82. trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  83. }
  84. return result
  85. }
  86. function clear(this: IterableCollections) {
  87. const target = toRaw(this)
  88. const hadItems = target.size !== 0
  89. const oldTarget = __DEV__
  90. ? target instanceof Map
  91. ? new Map(target)
  92. : new Set(target)
  93. : undefined
  94. // forward the operation before queueing reactions
  95. const result = getProto(target).clear.call(target)
  96. if (hadItems) {
  97. trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  98. }
  99. return result
  100. }
  101. function createForEach(isReadonly: boolean) {
  102. return function forEach(
  103. this: IterableCollections,
  104. callback: Function,
  105. thisArg?: unknown
  106. ) {
  107. const observed = this
  108. const target = toRaw(observed)
  109. const wrap = isReadonly ? toReadonly : toReactive
  110. track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  111. // important: create sure the callback is
  112. // 1. invoked with the reactive map as `this` and 3rd arg
  113. // 2. the value received should be a corresponding reactive/readonly.
  114. function wrappedCallback(value: unknown, key: unknown) {
  115. return callback.call(observed, wrap(value), wrap(key), observed)
  116. }
  117. return getProto(target).forEach.call(target, wrappedCallback, thisArg)
  118. }
  119. }
  120. function createIterableMethod(method: string | symbol, isReadonly: boolean) {
  121. return function(this: IterableCollections, ...args: unknown[]) {
  122. const target = toRaw(this)
  123. const isPair =
  124. method === 'entries' ||
  125. (method === Symbol.iterator && target instanceof Map)
  126. const innerIterator = getProto(target)[method].apply(target, args)
  127. const wrap = isReadonly ? toReadonly : toReactive
  128. track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  129. // return a wrapped iterator which returns observed versions of the
  130. // values emitted from the real iterator
  131. return {
  132. // iterator protocol
  133. next() {
  134. const { value, done } = innerIterator.next()
  135. return done
  136. ? { value, done }
  137. : {
  138. value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
  139. done
  140. }
  141. },
  142. // iterable protocol
  143. [Symbol.iterator]() {
  144. return this
  145. }
  146. }
  147. }
  148. }
  149. function createReadonlyMethod(
  150. method: Function,
  151. type: TriggerOpTypes
  152. ): Function {
  153. return function(this: CollectionTypes, ...args: unknown[]) {
  154. if (LOCKED) {
  155. if (__DEV__) {
  156. const key = args[0] ? `on key "${args[0]}" ` : ``
  157. console.warn(
  158. `${capitalize(type)} operation ${key}failed: target is readonly.`,
  159. toRaw(this)
  160. )
  161. }
  162. return type === TriggerOpTypes.DELETE ? false : this
  163. } else {
  164. return method.apply(this, args)
  165. }
  166. }
  167. }
  168. const mutableInstrumentations: Record<string, Function> = {
  169. get(this: MapTypes, key: unknown) {
  170. return get(this, key, toReactive)
  171. },
  172. get size() {
  173. return size((this as unknown) as IterableCollections)
  174. },
  175. has,
  176. add,
  177. set,
  178. delete: deleteEntry,
  179. clear,
  180. forEach: createForEach(false)
  181. }
  182. const readonlyInstrumentations: Record<string, Function> = {
  183. get(this: MapTypes, key: unknown) {
  184. return get(this, key, toReadonly)
  185. },
  186. get size(this: IterableCollections) {
  187. return size(this)
  188. },
  189. has,
  190. add: createReadonlyMethod(add, TriggerOpTypes.ADD),
  191. set: createReadonlyMethod(set, TriggerOpTypes.SET),
  192. delete: createReadonlyMethod(deleteEntry, TriggerOpTypes.DELETE),
  193. clear: createReadonlyMethod(clear, TriggerOpTypes.CLEAR),
  194. forEach: createForEach(true)
  195. }
  196. const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
  197. iteratorMethods.forEach(method => {
  198. mutableInstrumentations[method as string] = createIterableMethod(
  199. method,
  200. false
  201. )
  202. readonlyInstrumentations[method as string] = createIterableMethod(
  203. method,
  204. true
  205. )
  206. })
  207. function createInstrumentationGetter(
  208. instrumentations: Record<string, Function>
  209. ) {
  210. return (
  211. target: CollectionTypes,
  212. key: string | symbol,
  213. receiver: CollectionTypes
  214. ) =>
  215. Reflect.get(
  216. hasOwn(instrumentations, key) && key in target
  217. ? instrumentations
  218. : target,
  219. key,
  220. receiver
  221. )
  222. }
  223. export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  224. get: createInstrumentationGetter(mutableInstrumentations)
  225. }
  226. export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
  227. get: createInstrumentationGetter(readonlyInstrumentations)
  228. }