Skip to content

Commit d901b6b

Browse files
committed
refactor(reactivity): use more efficient reactive checks
WeakSets and WeakMaps shows degrading performance as the amount of observed objects increases. Using hidden keys result in better performance especially when repeatedly creating large amounts of reactive proxies. This also makes it possible to more efficiently declare non-reactive objects in userland.
1 parent 36972c2 commit d901b6b

File tree

13 files changed

+145
-78
lines changed

13 files changed

+145
-78
lines changed

packages/reactivity/src/baseHandlers.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { reactive, readonly, toRaw } from './reactive'
1+
import { reactive, readonly, toRaw, ReactiveFlags } from './reactive'
22
import { TrackOpTypes, TriggerOpTypes } from './operations'
33
import { track, trigger, ITERATE_KEY } from './effect'
44
import { isObject, hasOwn, isSymbol, hasChanged, isArray } from '@vue/shared'
@@ -35,6 +35,14 @@ const arrayInstrumentations: Record<string, Function> = {}
3535

3636
function createGetter(isReadonly = false, shallow = false) {
3737
return function get(target: object, key: string | symbol, receiver: object) {
38+
if (key === ReactiveFlags.isReactive) {
39+
return !isReadonly
40+
} else if (key === ReactiveFlags.isReadonly) {
41+
return isReadonly
42+
} else if (key === ReactiveFlags.raw) {
43+
return target
44+
}
45+
3846
const targetIsArray = isArray(target)
3947
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
4048
return Reflect.get(arrayInstrumentations, key, receiver)

packages/reactivity/src/collectionHandlers.ts

+19-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { toRaw, reactive, readonly } from './reactive'
1+
import { toRaw, reactive, readonly, ReactiveFlags } from './reactive'
22
import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
33
import { TrackOpTypes, TriggerOpTypes } from './operations'
44
import {
@@ -242,29 +242,40 @@ iteratorMethods.forEach(method => {
242242
)
243243
})
244244

245-
function createInstrumentationGetter(
246-
instrumentations: Record<string, Function>
247-
) {
245+
function createInstrumentationGetter(isReadonly: boolean) {
246+
const instrumentations = isReadonly
247+
? readonlyInstrumentations
248+
: mutableInstrumentations
249+
248250
return (
249251
target: CollectionTypes,
250252
key: string | symbol,
251253
receiver: CollectionTypes
252-
) =>
253-
Reflect.get(
254+
) => {
255+
if (key === ReactiveFlags.isReactive) {
256+
return !isReadonly
257+
} else if (key === ReactiveFlags.isReadonly) {
258+
return isReadonly
259+
} else if (key === ReactiveFlags.raw) {
260+
return target
261+
}
262+
263+
return Reflect.get(
254264
hasOwn(instrumentations, key) && key in target
255265
? instrumentations
256266
: target,
257267
key,
258268
receiver
259269
)
270+
}
260271
}
261272

262273
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
263-
get: createInstrumentationGetter(mutableInstrumentations)
274+
get: createInstrumentationGetter(false)
264275
}
265276

266277
export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
267-
get: createInstrumentationGetter(readonlyInstrumentations)
278+
get: createInstrumentationGetter(true)
268279
}
269280

270281
function checkIdentityKeys(

packages/reactivity/src/computed.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function computed<T>(
5757
}
5858
})
5959
computed = {
60-
_isRef: true,
60+
__v_isRef: true,
6161
// expose effect so computed can be stopped
6262
effect: runner,
6363
get value() {

packages/reactivity/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export {
2121
shallowReactive,
2222
shallowReadonly,
2323
markRaw,
24-
toRaw
24+
toRaw,
25+
ReactiveFlags
2526
} from './reactive'
2627
export {
2728
computed,

packages/reactivity/src/reactive.ts

+50-37
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isObject, toRawType } from '@vue/shared'
1+
import { isObject, toRawType, def } from '@vue/shared'
22
import {
33
mutableHandlers,
44
readonlyHandlers,
@@ -13,25 +13,38 @@ import { UnwrapRef, Ref } from './ref'
1313
import { makeMap } from '@vue/shared'
1414

1515
// WeakMaps that store {raw <-> observed} pairs.
16-
const rawToReactive = new WeakMap<any, any>()
17-
const reactiveToRaw = new WeakMap<any, any>()
18-
const rawToReadonly = new WeakMap<any, any>()
19-
const readonlyToRaw = new WeakMap<any, any>()
16+
// const rawToReactive = new WeakMap<any, any>()
17+
// const reactiveToRaw = new WeakMap<any, any>()
18+
// const rawToReadonly = new WeakMap<any, any>()
19+
// const readonlyToRaw = new WeakMap<any, any>()
2020

21-
// WeakSets for values that are marked readonly or non-reactive during
22-
// observable creation.
23-
const rawValues = new WeakSet<any>()
21+
export const enum ReactiveFlags {
22+
skip = '__v_skip',
23+
isReactive = '__v_isReactive',
24+
isReadonly = '__v_isReadonly',
25+
raw = '__v_raw',
26+
reactive = '__v_reactive',
27+
readonly = '__v_readonly'
28+
}
29+
30+
interface Target {
31+
__v_skip?: boolean
32+
__v_isReactive?: boolean
33+
__v_isReadonly?: boolean
34+
__v_raw?: any
35+
__v_reactive?: any
36+
__v_readonly?: any
37+
}
2438

2539
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
2640
const isObservableType = /*#__PURE__*/ makeMap(
2741
'Object,Array,Map,Set,WeakMap,WeakSet'
2842
)
2943

30-
const canObserve = (value: any): boolean => {
44+
const canObserve = (value: Target): boolean => {
3145
return (
32-
!value._isVNode &&
46+
!value.__v_skip &&
3347
isObservableType(toRawType(value)) &&
34-
!rawValues.has(value) &&
3548
!Object.isFrozen(value)
3649
)
3750
}
@@ -42,13 +55,12 @@ type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
4255
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
4356
export function reactive(target: object) {
4457
// if trying to observe a readonly proxy, return the readonly version.
45-
if (readonlyToRaw.has(target)) {
58+
if (target && (target as Target).__v_isReadonly) {
4659
return target
4760
}
4861
return createReactiveObject(
4962
target,
50-
rawToReactive,
51-
reactiveToRaw,
63+
false,
5264
mutableHandlers,
5365
mutableCollectionHandlers
5466
)
@@ -60,8 +72,7 @@ export function reactive(target: object) {
6072
export function shallowReactive<T extends object>(target: T): T {
6173
return createReactiveObject(
6274
target,
63-
rawToReactive,
64-
reactiveToRaw,
75+
false,
6576
shallowReactiveHandlers,
6677
mutableCollectionHandlers
6778
)
@@ -72,8 +83,7 @@ export function readonly<T extends object>(
7283
): Readonly<UnwrapNestedRefs<T>> {
7384
return createReactiveObject(
7485
target,
75-
rawToReadonly,
76-
readonlyToRaw,
86+
true,
7787
readonlyHandlers,
7888
readonlyCollectionHandlers
7989
)
@@ -88,17 +98,15 @@ export function shallowReadonly<T extends object>(
8898
): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
8999
return createReactiveObject(
90100
target,
91-
rawToReadonly,
92-
readonlyToRaw,
101+
true,
93102
shallowReadonlyHandlers,
94103
readonlyCollectionHandlers
95104
)
96105
}
97106

98107
function createReactiveObject(
99-
target: unknown,
100-
toProxy: WeakMap<any, any>,
101-
toRaw: WeakMap<any, any>,
108+
target: Target,
109+
isReadonly: boolean,
102110
baseHandlers: ProxyHandler<any>,
103111
collectionHandlers: ProxyHandler<any>
104112
) {
@@ -108,15 +116,16 @@ function createReactiveObject(
108116
}
109117
return target
110118
}
119+
// target is already a Proxy, return it.
120+
// excpetion: calling readonly() on a reactive object
121+
if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
122+
return target
123+
}
111124
// target already has corresponding Proxy
112-
let observed = toProxy.get(target)
125+
let observed = isReadonly ? target.__v_readonly : target.__v_reactive
113126
if (observed !== void 0) {
114127
return observed
115128
}
116-
// target is already a Proxy
117-
if (toRaw.has(target)) {
118-
return target
119-
}
120129
// only a whitelist of value types can be observed.
121130
if (!canObserve(target)) {
122131
return target
@@ -125,30 +134,34 @@ function createReactiveObject(
125134
? collectionHandlers
126135
: baseHandlers
127136
observed = new Proxy(target, handlers)
128-
toProxy.set(target, observed)
129-
toRaw.set(observed, target)
137+
def(
138+
target,
139+
isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive,
140+
observed
141+
)
130142
return observed
131143
}
132144

133145
export function isReactive(value: unknown): boolean {
134-
value = readonlyToRaw.get(value) || value
135-
return reactiveToRaw.has(value)
146+
if (isReadonly(value)) {
147+
return isReactive((value as Target).__v_raw)
148+
}
149+
return !!(value && (value as Target).__v_isReactive)
136150
}
137151

138152
export function isReadonly(value: unknown): boolean {
139-
return readonlyToRaw.has(value)
153+
return !!(value && (value as Target).__v_isReadonly)
140154
}
141155

142156
export function isProxy(value: unknown): boolean {
143-
return readonlyToRaw.has(value) || reactiveToRaw.has(value)
157+
return isReactive(value) || isReadonly(value)
144158
}
145159

146160
export function toRaw<T>(observed: T): T {
147-
observed = readonlyToRaw.get(observed) || observed
148-
return reactiveToRaw.get(observed) || observed
161+
return (observed && toRaw((observed as Target).__v_raw)) || observed
149162
}
150163

151164
export function markRaw<T extends object>(value: T): T {
152-
rawValues.add(value)
165+
def(value, ReactiveFlags.skip, true)
153166
return value
154167
}

packages/reactivity/src/ref.ts

+8-15
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,11 @@ import { reactive, isProxy, toRaw } from './reactive'
55
import { ComputedRef } from './computed'
66
import { CollectionTypes } from './collectionHandlers'
77

8-
const isRefSymbol = Symbol()
9-
108
export interface Ref<T = any> {
11-
// This field is necessary to allow TS to differentiate a Ref from a plain
12-
// object that happens to have a "value" field.
13-
// However, checking a symbol on an arbitrary object is much slower than
14-
// checking a plain property, so we use a _isRef plain property for isRef()
15-
// check in the actual implementation.
16-
// The reason for not just declaring _isRef in the interface is because we
17-
// don't want this internal field to leak into userland autocompletion -
18-
// a private symbol, on the other hand, achieves just that.
19-
[isRefSymbol]: true
9+
/**
10+
* @internal
11+
*/
12+
__v_isRef: true
2013
value: T
2114
}
2215

@@ -27,7 +20,7 @@ const convert = <T extends unknown>(val: T): T =>
2720

2821
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
2922
export function isRef(r: any): r is Ref {
30-
return r ? r._isRef === true : false
23+
return r ? r.__v_isRef === true : false
3124
}
3225

3326
export function ref<T extends object>(
@@ -51,7 +44,7 @@ function createRef(rawValue: unknown, shallow = false) {
5144
}
5245
let value = shallow ? rawValue : convert(rawValue)
5346
const r = {
54-
_isRef: true,
47+
__v_isRef: true,
5548
get value() {
5649
track(r, TrackOpTypes.GET, 'value')
5750
return value
@@ -99,7 +92,7 @@ export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
9992
() => trigger(r, TriggerOpTypes.SET, 'value')
10093
)
10194
const r = {
102-
_isRef: true,
95+
__v_isRef: true,
10396
get value() {
10497
return get()
10598
},
@@ -126,7 +119,7 @@ export function toRef<T extends object, K extends keyof T>(
126119
key: K
127120
): Ref<T[K]> {
128121
return {
129-
_isRef: true,
122+
__v_isRef: true,
130123
get value(): any {
131124
return object[key]
132125
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { render, h, nodeOps, reactive, isReactive } from '@vue/runtime-test'
2+
3+
describe('misc', () => {
4+
test('component public instance should not be observable', () => {
5+
let instance: any
6+
const Comp = {
7+
render() {},
8+
mounted() {
9+
instance = this
10+
}
11+
}
12+
render(h(Comp), nodeOps.createElement('div'))
13+
expect(instance).toBeDefined()
14+
const r = reactive(instance)
15+
expect(r).toBe(instance)
16+
expect(isReactive(r)).toBe(false)
17+
})
18+
})

packages/runtime-core/__tests__/vnode.spec.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '../src/vnode'
1313
import { Data } from '../src/component'
1414
import { ShapeFlags, PatchFlags } from '@vue/shared'
15-
import { h } from '../src'
15+
import { h, reactive, isReactive } from '../src'
1616
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
1717

1818
describe('vnode', () => {
@@ -425,5 +425,12 @@ describe('vnode', () => {
425425
createApp(App).mount(root)
426426
expect(serializeInner(root)).toBe('<h1>Root Component</h1>')
427427
})
428+
429+
test('should not be observable', () => {
430+
const a = createVNode('div')
431+
const b = reactive(a)
432+
expect(b).toBe(a)
433+
expect(isReactive(b)).toBe(false)
434+
})
428435
})
429436
})

packages/runtime-core/src/component.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import {
44
ReactiveEffect,
55
pauseTracking,
66
resetTracking,
7-
shallowReadonly,
8-
markRaw
7+
shallowReadonly
98
} from '@vue/reactivity'
109
import {
1110
ComponentPublicInstance,
@@ -464,7 +463,7 @@ function setupStatefulComponent(
464463
instance.accessCache = {}
465464
// 1. create public instance / render proxy
466465
// also mark it raw so it's never observed
467-
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
466+
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
468467
if (__DEV__) {
469468
exposePropsOnRenderContext(instance)
470469
}

0 commit comments

Comments
 (0)