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