reactive.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. import { def, isObject, toRawType } from '@vue/shared'
  2. import {
  3. mutableHandlers,
  4. readonlyHandlers,
  5. shallowReactiveHandlers,
  6. shallowReadonlyHandlers,
  7. } from './baseHandlers'
  8. import {
  9. mutableCollectionHandlers,
  10. readonlyCollectionHandlers,
  11. shallowCollectionHandlers,
  12. shallowReadonlyCollectionHandlers,
  13. } from './collectionHandlers'
  14. import type { RawSymbol, Ref, UnwrapRefSimple } from './ref'
  15. import { ReactiveFlags } from './constants'
  16. import { warn } from './warning'
  17. export interface Target {
  18. [ReactiveFlags.SKIP]?: boolean
  19. [ReactiveFlags.IS_REACTIVE]?: boolean
  20. [ReactiveFlags.IS_READONLY]?: boolean
  21. [ReactiveFlags.IS_SHALLOW]?: boolean
  22. [ReactiveFlags.RAW]?: any
  23. }
  24. export const reactiveMap = new WeakMap<Target, any>()
  25. export const shallowReactiveMap = new WeakMap<Target, any>()
  26. export const readonlyMap = new WeakMap<Target, any>()
  27. export const shallowReadonlyMap = new WeakMap<Target, any>()
  28. enum TargetType {
  29. INVALID = 0,
  30. COMMON = 1,
  31. COLLECTION = 2,
  32. }
  33. function targetTypeMap(rawType: string) {
  34. switch (rawType) {
  35. case 'Object':
  36. case 'Array':
  37. return TargetType.COMMON
  38. case 'Map':
  39. case 'Set':
  40. case 'WeakMap':
  41. case 'WeakSet':
  42. return TargetType.COLLECTION
  43. default:
  44. return TargetType.INVALID
  45. }
  46. }
  47. function getTargetType(value: Target) {
  48. return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
  49. ? TargetType.INVALID
  50. : targetTypeMap(toRawType(value))
  51. }
  52. // only unwrap nested ref
  53. export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
  54. declare const ReactiveMarkerSymbol: unique symbol
  55. export declare class ReactiveMarker {
  56. private [ReactiveMarkerSymbol]?: void
  57. }
  58. export type Reactive<T> = UnwrapNestedRefs<T> &
  59. (T extends readonly any[] ? ReactiveMarker : {})
  60. /**
  61. * Returns a reactive proxy of the object.
  62. *
  63. * The reactive conversion is "deep": it affects all nested properties. A
  64. * reactive object also deeply unwraps any properties that are refs while
  65. * maintaining reactivity.
  66. *
  67. * @example
  68. * ```js
  69. * const obj = reactive({ count: 0 })
  70. * ```
  71. *
  72. * @param target - The source object.
  73. * @see {@link https://vuejs.org/api/reactivity-core.html#reactive}
  74. */
  75. export function reactive<T extends object>(target: T): Reactive<T>
  76. export function reactive(target: object) {
  77. // if trying to observe a readonly proxy, return the readonly version.
  78. if (isReadonly(target)) {
  79. return target
  80. }
  81. return createReactiveObject(
  82. target,
  83. false,
  84. mutableHandlers,
  85. mutableCollectionHandlers,
  86. reactiveMap,
  87. )
  88. }
  89. export declare const ShallowReactiveMarker: unique symbol
  90. export type ShallowReactive<T> = T & { [ShallowReactiveMarker]?: true }
  91. /**
  92. * Shallow version of {@link reactive()}.
  93. *
  94. * Unlike {@link reactive()}, there is no deep conversion: only root-level
  95. * properties are reactive for a shallow reactive object. Property values are
  96. * stored and exposed as-is - this also means properties with ref values will
  97. * not be automatically unwrapped.
  98. *
  99. * @example
  100. * ```js
  101. * const state = shallowReactive({
  102. * foo: 1,
  103. * nested: {
  104. * bar: 2
  105. * }
  106. * })
  107. *
  108. * // mutating state's own properties is reactive
  109. * state.foo++
  110. *
  111. * // ...but does not convert nested objects
  112. * isReactive(state.nested) // false
  113. *
  114. * // NOT reactive
  115. * state.nested.bar++
  116. * ```
  117. *
  118. * @param target - The source object.
  119. * @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowreactive}
  120. */
  121. export function shallowReactive<T extends object>(
  122. target: T,
  123. ): ShallowReactive<T> {
  124. return createReactiveObject(
  125. target,
  126. false,
  127. shallowReactiveHandlers,
  128. shallowCollectionHandlers,
  129. shallowReactiveMap,
  130. )
  131. }
  132. type Primitive = string | number | boolean | bigint | symbol | undefined | null
  133. export type Builtin = Primitive | Function | Date | Error | RegExp
  134. export type DeepReadonly<T> = T extends Builtin
  135. ? T
  136. : T extends Map<infer K, infer V>
  137. ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
  138. : T extends ReadonlyMap<infer K, infer V>
  139. ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
  140. : T extends WeakMap<infer K, infer V>
  141. ? WeakMap<DeepReadonly<K>, DeepReadonly<V>>
  142. : T extends Set<infer U>
  143. ? ReadonlySet<DeepReadonly<U>>
  144. : T extends ReadonlySet<infer U>
  145. ? ReadonlySet<DeepReadonly<U>>
  146. : T extends WeakSet<infer U>
  147. ? WeakSet<DeepReadonly<U>>
  148. : T extends Promise<infer U>
  149. ? Promise<DeepReadonly<U>>
  150. : T extends Ref<infer U>
  151. ? Readonly<Ref<DeepReadonly<U>>>
  152. : T extends {}
  153. ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  154. : Readonly<T>
  155. /**
  156. * Takes an object (reactive or plain) or a ref and returns a readonly proxy to
  157. * the original.
  158. *
  159. * A readonly proxy is deep: any nested property accessed will be readonly as
  160. * well. It also has the same ref-unwrapping behavior as {@link reactive()},
  161. * except the unwrapped values will also be made readonly.
  162. *
  163. * @example
  164. * ```js
  165. * const original = reactive({ count: 0 })
  166. *
  167. * const copy = readonly(original)
  168. *
  169. * watchEffect(() => {
  170. * // works for reactivity tracking
  171. * console.log(copy.count)
  172. * })
  173. *
  174. * // mutating original will trigger watchers relying on the copy
  175. * original.count++
  176. *
  177. * // mutating the copy will fail and result in a warning
  178. * copy.count++ // warning!
  179. * ```
  180. *
  181. * @param target - The source object.
  182. * @see {@link https://vuejs.org/api/reactivity-core.html#readonly}
  183. */
  184. export function readonly<T extends object>(
  185. target: T,
  186. ): DeepReadonly<UnwrapNestedRefs<T>> {
  187. return createReactiveObject(
  188. target,
  189. true,
  190. readonlyHandlers,
  191. readonlyCollectionHandlers,
  192. readonlyMap,
  193. )
  194. }
  195. /**
  196. * Shallow version of {@link readonly()}.
  197. *
  198. * Unlike {@link readonly()}, there is no deep conversion: only root-level
  199. * properties are made readonly. Property values are stored and exposed as-is -
  200. * this also means properties with ref values will not be automatically
  201. * unwrapped.
  202. *
  203. * @example
  204. * ```js
  205. * const state = shallowReadonly({
  206. * foo: 1,
  207. * nested: {
  208. * bar: 2
  209. * }
  210. * })
  211. *
  212. * // mutating state's own properties will fail
  213. * state.foo++
  214. *
  215. * // ...but works on nested objects
  216. * isReadonly(state.nested) // false
  217. *
  218. * // works
  219. * state.nested.bar++
  220. * ```
  221. *
  222. * @param target - The source object.
  223. * @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowreadonly}
  224. */
  225. export function shallowReadonly<T extends object>(target: T): Readonly<T> {
  226. return createReactiveObject(
  227. target,
  228. true,
  229. shallowReadonlyHandlers,
  230. shallowReadonlyCollectionHandlers,
  231. shallowReadonlyMap,
  232. )
  233. }
  234. function createReactiveObject(
  235. target: Target,
  236. isReadonly: boolean,
  237. baseHandlers: ProxyHandler<any>,
  238. collectionHandlers: ProxyHandler<any>,
  239. proxyMap: WeakMap<Target, any>,
  240. ) {
  241. if (!isObject(target)) {
  242. if (__DEV__) {
  243. warn(
  244. `value cannot be made ${isReadonly ? 'readonly' : 'reactive'}: ${String(
  245. target,
  246. )}`,
  247. )
  248. }
  249. return target
  250. }
  251. // target is already a Proxy, return it.
  252. // exception: calling readonly() on a reactive object
  253. if (
  254. target[ReactiveFlags.RAW] &&
  255. !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  256. ) {
  257. return target
  258. }
  259. // target already has corresponding Proxy
  260. const existingProxy = proxyMap.get(target)
  261. if (existingProxy) {
  262. return existingProxy
  263. }
  264. // only specific value types can be observed.
  265. const targetType = getTargetType(target)
  266. if (targetType === TargetType.INVALID) {
  267. return target
  268. }
  269. const proxy = new Proxy(
  270. target,
  271. targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  272. )
  273. proxyMap.set(target, proxy)
  274. return proxy
  275. }
  276. /**
  277. * Checks if an object is a proxy created by {@link reactive()} or
  278. * {@link shallowReactive()} (or {@link ref()} in some cases).
  279. *
  280. * @example
  281. * ```js
  282. * isReactive(reactive({})) // => true
  283. * isReactive(readonly(reactive({}))) // => true
  284. * isReactive(ref({}).value) // => true
  285. * isReactive(readonly(ref({})).value) // => true
  286. * isReactive(ref(true)) // => false
  287. * isReactive(shallowRef({}).value) // => false
  288. * isReactive(shallowReactive({})) // => true
  289. * ```
  290. *
  291. * @param value - The value to check.
  292. * @see {@link https://vuejs.org/api/reactivity-utilities.html#isreactive}
  293. */
  294. export function isReactive(value: unknown): boolean {
  295. if (isReadonly(value)) {
  296. return isReactive((value as Target)[ReactiveFlags.RAW])
  297. }
  298. return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
  299. }
  300. /**
  301. * Checks whether the passed value is a readonly object. The properties of a
  302. * readonly object can change, but they can't be assigned directly via the
  303. * passed object.
  304. *
  305. * The proxies created by {@link readonly()} and {@link shallowReadonly()} are
  306. * both considered readonly, as is a computed ref without a set function.
  307. *
  308. * @param value - The value to check.
  309. * @see {@link https://vuejs.org/api/reactivity-utilities.html#isreadonly}
  310. */
  311. export function isReadonly(value: unknown): boolean {
  312. return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
  313. }
  314. export function isShallow(value: unknown): boolean {
  315. return !!(value && (value as Target)[ReactiveFlags.IS_SHALLOW])
  316. }
  317. /**
  318. * Checks if an object is a proxy created by {@link reactive},
  319. * {@link readonly}, {@link shallowReactive} or {@link shallowReadonly()}.
  320. *
  321. * @param value - The value to check.
  322. * @see {@link https://vuejs.org/api/reactivity-utilities.html#isproxy}
  323. */
  324. export function isProxy(value: any): boolean {
  325. return value ? !!value[ReactiveFlags.RAW] : false
  326. }
  327. /**
  328. * Returns the raw, original object of a Vue-created proxy.
  329. *
  330. * `toRaw()` can return the original object from proxies created by
  331. * {@link reactive()}, {@link readonly()}, {@link shallowReactive()} or
  332. * {@link shallowReadonly()}.
  333. *
  334. * This is an escape hatch that can be used to temporarily read without
  335. * incurring proxy access / tracking overhead or write without triggering
  336. * changes. It is **not** recommended to hold a persistent reference to the
  337. * original object. Use with caution.
  338. *
  339. * @example
  340. * ```js
  341. * const foo = {}
  342. * const reactiveFoo = reactive(foo)
  343. *
  344. * console.log(toRaw(reactiveFoo) === foo) // true
  345. * ```
  346. *
  347. * @param observed - The object for which the "raw" value is requested.
  348. * @see {@link https://vuejs.org/api/reactivity-advanced.html#toraw}
  349. */
  350. export function toRaw<T>(observed: T): T {
  351. const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  352. return raw ? toRaw(raw) : observed
  353. }
  354. export type Raw<T> = T & { [RawSymbol]?: true }
  355. /**
  356. * Marks an object so that it will never be converted to a proxy. Returns the
  357. * object itself.
  358. *
  359. * @example
  360. * ```js
  361. * const foo = markRaw({})
  362. * console.log(isReactive(reactive(foo))) // false
  363. *
  364. * // also works when nested inside other reactive objects
  365. * const bar = reactive({ foo })
  366. * console.log(isReactive(bar.foo)) // false
  367. * ```
  368. *
  369. * **Warning:** `markRaw()` together with the shallow APIs such as
  370. * {@link shallowReactive()} allow you to selectively opt-out of the default
  371. * deep reactive/readonly conversion and embed raw, non-proxied objects in your
  372. * state graph.
  373. *
  374. * @param value - The object to be marked as "raw".
  375. * @see {@link https://vuejs.org/api/reactivity-advanced.html#markraw}
  376. */
  377. export function markRaw<T extends object>(value: T): Raw<T> {
  378. if (Object.isExtensible(value)) {
  379. def(value, ReactiveFlags.SKIP, true)
  380. }
  381. return value
  382. }
  383. /**
  384. * Returns a reactive proxy of the given value (if possible).
  385. *
  386. * If the given value is not an object, the original value itself is returned.
  387. *
  388. * @param value - The value for which a reactive proxy shall be created.
  389. */
  390. export const toReactive = <T extends unknown>(value: T): T =>
  391. isObject(value) ? reactive(value) : value
  392. /**
  393. * Returns a readonly proxy of the given value (if possible).
  394. *
  395. * If the given value is not an object, the original value itself is returned.
  396. *
  397. * @param value - The value for which a readonly proxy shall be created.
  398. */
  399. export const toReadonly = <T extends unknown>(value: T): DeepReadonly<T> =>
  400. isObject(value) ? readonly(value) : (value as DeepReadonly<T>)