Skip to content

Commit

Permalink
refactor(reactivity): use more efficient reactive checks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
yyx990803 committed May 2, 2020
1 parent 36972c2 commit d901b6b
Show file tree
Hide file tree
Showing 13 changed files with 145 additions and 78 deletions.
10 changes: 9 additions & 1 deletion packages/reactivity/src/baseHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { reactive, readonly, toRaw } from './reactive'
import { reactive, readonly, toRaw, ReactiveFlags } from './reactive'
import { TrackOpTypes, TriggerOpTypes } from './operations'
import { track, trigger, ITERATE_KEY } from './effect'
import { isObject, hasOwn, isSymbol, hasChanged, isArray } from '@vue/shared'
Expand Down Expand Up @@ -35,6 +35,14 @@ const arrayInstrumentations: Record<string, Function> = {}

function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.isReactive) {
return !isReadonly
} else if (key === ReactiveFlags.isReadonly) {
return isReadonly
} else if (key === ReactiveFlags.raw) {
return target
}

const targetIsArray = isArray(target)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
Expand Down
27 changes: 19 additions & 8 deletions packages/reactivity/src/collectionHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toRaw, reactive, readonly } from './reactive'
import { toRaw, reactive, readonly, ReactiveFlags } from './reactive'
import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
import { TrackOpTypes, TriggerOpTypes } from './operations'
import {
Expand Down Expand Up @@ -242,29 +242,40 @@ iteratorMethods.forEach(method => {
)
})

function createInstrumentationGetter(
instrumentations: Record<string, Function>
) {
function createInstrumentationGetter(isReadonly: boolean) {
const instrumentations = isReadonly
? readonlyInstrumentations
: mutableInstrumentations

return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) =>
Reflect.get(
) => {
if (key === ReactiveFlags.isReactive) {
return !isReadonly
} else if (key === ReactiveFlags.isReadonly) {
return isReadonly
} else if (key === ReactiveFlags.raw) {
return target
}

return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(mutableInstrumentations)
get: createInstrumentationGetter(false)
}

export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(readonlyInstrumentations)
get: createInstrumentationGetter(true)
}

function checkIdentityKeys(
Expand Down
2 changes: 1 addition & 1 deletion packages/reactivity/src/computed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function computed<T>(
}
})
computed = {
_isRef: true,
__v_isRef: true,
// expose effect so computed can be stopped
effect: runner,
get value() {
Expand Down
3 changes: 2 additions & 1 deletion packages/reactivity/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export {
shallowReactive,
shallowReadonly,
markRaw,
toRaw
toRaw,
ReactiveFlags
} from './reactive'
export {
computed,
Expand Down
87 changes: 50 additions & 37 deletions packages/reactivity/src/reactive.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isObject, toRawType } from '@vue/shared'
import { isObject, toRawType, def } from '@vue/shared'
import {
mutableHandlers,
readonlyHandlers,
Expand All @@ -13,25 +13,38 @@ import { UnwrapRef, Ref } from './ref'
import { makeMap } from '@vue/shared'

// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
// const rawToReactive = new WeakMap<any, any>()
// const reactiveToRaw = new WeakMap<any, any>()
// const rawToReadonly = new WeakMap<any, any>()
// const readonlyToRaw = new WeakMap<any, any>()

// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const rawValues = new WeakSet<any>()
export const enum ReactiveFlags {
skip = '__v_skip',
isReactive = '__v_isReactive',
isReadonly = '__v_isReadonly',
raw = '__v_raw',
reactive = '__v_reactive',
readonly = '__v_readonly'
}

interface Target {
__v_skip?: boolean
__v_isReactive?: boolean
__v_isReadonly?: boolean
__v_raw?: any
__v_reactive?: any
__v_readonly?: any
}

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

const canObserve = (value: any): boolean => {
const canObserve = (value: Target): boolean => {
return (
!value._isVNode &&
!value.__v_skip &&
isObservableType(toRawType(value)) &&
!rawValues.has(value) &&
!Object.isFrozen(value)
)
}
Expand All @@ -42,13 +55,12 @@ type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (readonlyToRaw.has(target)) {
if (target && (target as Target).__v_isReadonly) {
return target
}
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
false,
mutableHandlers,
mutableCollectionHandlers
)
Expand All @@ -60,8 +72,7 @@ export function reactive(target: object) {
export function shallowReactive<T extends object>(target: T): T {
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
false,
shallowReactiveHandlers,
mutableCollectionHandlers
)
Expand All @@ -72,8 +83,7 @@ export function readonly<T extends object>(
): Readonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
rawToReadonly,
readonlyToRaw,
true,
readonlyHandlers,
readonlyCollectionHandlers
)
Expand All @@ -88,17 +98,15 @@ export function shallowReadonly<T extends object>(
): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
return createReactiveObject(
target,
rawToReadonly,
readonlyToRaw,
true,
shallowReadonlyHandlers,
readonlyCollectionHandlers
)
}

function createReactiveObject(
target: unknown,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
Expand All @@ -108,15 +116,16 @@ function createReactiveObject(
}
return target
}
// target is already a Proxy, return it.
// excpetion: calling readonly() on a reactive object
if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
return target
}
// target already has corresponding Proxy
let observed = toProxy.get(target)
let observed = isReadonly ? target.__v_readonly : target.__v_reactive
if (observed !== void 0) {
return observed
}
// target is already a Proxy
if (toRaw.has(target)) {
return target
}
// only a whitelist of value types can be observed.
if (!canObserve(target)) {
return target
Expand All @@ -125,30 +134,34 @@ function createReactiveObject(
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
def(
target,
isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive,
observed
)
return observed
}

export function isReactive(value: unknown): boolean {
value = readonlyToRaw.get(value) || value
return reactiveToRaw.has(value)
if (isReadonly(value)) {
return isReactive((value as Target).__v_raw)
}
return !!(value && (value as Target).__v_isReactive)
}

export function isReadonly(value: unknown): boolean {
return readonlyToRaw.has(value)
return !!(value && (value as Target).__v_isReadonly)
}

export function isProxy(value: unknown): boolean {
return readonlyToRaw.has(value) || reactiveToRaw.has(value)
return isReactive(value) || isReadonly(value)
}

export function toRaw<T>(observed: T): T {
observed = readonlyToRaw.get(observed) || observed
return reactiveToRaw.get(observed) || observed
return (observed && toRaw((observed as Target).__v_raw)) || observed
}

export function markRaw<T extends object>(value: T): T {
rawValues.add(value)
def(value, ReactiveFlags.skip, true)
return value
}
23 changes: 8 additions & 15 deletions packages/reactivity/src/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,11 @@ import { reactive, isProxy, toRaw } from './reactive'
import { ComputedRef } from './computed'
import { CollectionTypes } from './collectionHandlers'

const isRefSymbol = Symbol()

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

This comment has been minimized.

Copy link
@LinusBorg

LinusBorg May 4, 2020

Member

I think this change introduced a regression, see #1111

This comment has been minimized.

Copy link
@pikax

pikax May 4, 2020

Member

We need the symbol or this to be public, otherwise typescript has no way to distinguish from object with value from ref object

interface MyFakeValue{
  value: {
    a: 1
  }
}
value: T
}

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

export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
return r ? r._isRef === true : false
return r ? r.__v_isRef === true : false
}

export function ref<T extends object>(
Expand All @@ -51,7 +44,7 @@ function createRef(rawValue: unknown, shallow = false) {
}
let value = shallow ? rawValue : convert(rawValue)
const r = {
_isRef: true,
__v_isRef: true,
get value() {
track(r, TrackOpTypes.GET, 'value')
return value
Expand Down Expand Up @@ -99,7 +92,7 @@ export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
() => trigger(r, TriggerOpTypes.SET, 'value')
)
const r = {
_isRef: true,
__v_isRef: true,
get value() {
return get()
},
Expand All @@ -126,7 +119,7 @@ export function toRef<T extends object, K extends keyof T>(
key: K
): Ref<T[K]> {
return {
_isRef: true,
__v_isRef: true,
get value(): any {
return object[key]
},
Expand Down
18 changes: 18 additions & 0 deletions packages/runtime-core/__tests__/misc.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { render, h, nodeOps, reactive, isReactive } from '@vue/runtime-test'

describe('misc', () => {
test('component public instance should not be observable', () => {
let instance: any
const Comp = {
render() {},
mounted() {
instance = this
}
}
render(h(Comp), nodeOps.createElement('div'))
expect(instance).toBeDefined()
const r = reactive(instance)
expect(r).toBe(instance)
expect(isReactive(r)).toBe(false)
})
})
9 changes: 8 additions & 1 deletion packages/runtime-core/__tests__/vnode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '../src/vnode'
import { Data } from '../src/component'
import { ShapeFlags, PatchFlags } from '@vue/shared'
import { h } from '../src'
import { h, reactive, isReactive } from '../src'
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'

describe('vnode', () => {
Expand Down Expand Up @@ -425,5 +425,12 @@ describe('vnode', () => {
createApp(App).mount(root)
expect(serializeInner(root)).toBe('<h1>Root Component</h1>')
})

test('should not be observable', () => {
const a = createVNode('div')
const b = reactive(a)
expect(b).toBe(a)
expect(isReactive(b)).toBe(false)
})
})
})
5 changes: 2 additions & 3 deletions packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import {
ReactiveEffect,
pauseTracking,
resetTracking,
shallowReadonly,
markRaw
shallowReadonly
} from '@vue/reactivity'
import {
ComponentPublicInstance,
Expand Down Expand Up @@ -464,7 +463,7 @@ function setupStatefulComponent(
instance.accessCache = {}
// 1. create public instance / render proxy
// also mark it raw so it's never observed
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
if (__DEV__) {
exposePropsOnRenderContext(instance)
}
Expand Down
Loading

0 comments on commit d901b6b

Please sign in to comment.