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.
This commit is contained in:
Evan You 2020-05-02 16:16:51 -04:00
parent 36972c20b5
commit d901b6bea8
13 changed files with 145 additions and 78 deletions

View File

@ -1,4 +1,4 @@
import { reactive, readonly, toRaw } from './reactive' import { reactive, readonly, toRaw, ReactiveFlags } from './reactive'
import { TrackOpTypes, TriggerOpTypes } from './operations' import { TrackOpTypes, TriggerOpTypes } from './operations'
import { track, trigger, ITERATE_KEY } from './effect' import { track, trigger, ITERATE_KEY } from './effect'
import { isObject, hasOwn, isSymbol, hasChanged, isArray } from '@vue/shared' import { isObject, hasOwn, isSymbol, hasChanged, isArray } from '@vue/shared'
@ -35,6 +35,14 @@ const arrayInstrumentations: Record<string, Function> = {}
function createGetter(isReadonly = false, shallow = false) { function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) { 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) const targetIsArray = isArray(target)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) { if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver) return Reflect.get(arrayInstrumentations, key, receiver)

View File

@ -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 { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
import { TrackOpTypes, TriggerOpTypes } from './operations' import { TrackOpTypes, TriggerOpTypes } from './operations'
import { import {
@ -242,15 +242,25 @@ iteratorMethods.forEach(method => {
) )
}) })
function createInstrumentationGetter( function createInstrumentationGetter(isReadonly: boolean) {
instrumentations: Record<string, Function> const instrumentations = isReadonly
) { ? readonlyInstrumentations
: mutableInstrumentations
return ( return (
target: CollectionTypes, target: CollectionTypes,
key: string | symbol, key: string | symbol,
receiver: CollectionTypes 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 hasOwn(instrumentations, key) && key in target
? instrumentations ? instrumentations
: target, : target,
@ -258,13 +268,14 @@ function createInstrumentationGetter(
receiver receiver
) )
} }
}
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = { export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(mutableInstrumentations) get: createInstrumentationGetter(false)
} }
export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = { export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(readonlyInstrumentations) get: createInstrumentationGetter(true)
} }
function checkIdentityKeys( function checkIdentityKeys(

View File

@ -57,7 +57,7 @@ export function computed<T>(
} }
}) })
computed = { computed = {
_isRef: true, __v_isRef: true,
// expose effect so computed can be stopped // expose effect so computed can be stopped
effect: runner, effect: runner,
get value() { get value() {

View File

@ -21,7 +21,8 @@ export {
shallowReactive, shallowReactive,
shallowReadonly, shallowReadonly,
markRaw, markRaw,
toRaw toRaw,
ReactiveFlags
} from './reactive' } from './reactive'
export { export {
computed, computed,

View File

@ -1,4 +1,4 @@
import { isObject, toRawType } from '@vue/shared' import { isObject, toRawType, def } from '@vue/shared'
import { import {
mutableHandlers, mutableHandlers,
readonlyHandlers, readonlyHandlers,
@ -13,25 +13,38 @@ import { UnwrapRef, Ref } from './ref'
import { makeMap } from '@vue/shared' import { makeMap } from '@vue/shared'
// WeakMaps that store {raw <-> observed} pairs. // WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>() // const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>() // const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>() // const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>() // const readonlyToRaw = new WeakMap<any, any>()
// WeakSets for values that are marked readonly or non-reactive during export const enum ReactiveFlags {
// observable creation. skip = '__v_skip',
const rawValues = new WeakSet<any>() 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 collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
const isObservableType = /*#__PURE__*/ makeMap( const isObservableType = /*#__PURE__*/ makeMap(
'Object,Array,Map,Set,WeakMap,WeakSet' 'Object,Array,Map,Set,WeakMap,WeakSet'
) )
const canObserve = (value: any): boolean => { const canObserve = (value: Target): boolean => {
return ( return (
!value._isVNode && !value.__v_skip &&
isObservableType(toRawType(value)) && isObservableType(toRawType(value)) &&
!rawValues.has(value) &&
!Object.isFrozen(value) !Object.isFrozen(value)
) )
} }
@ -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<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) { export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version. // 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 target
} }
return createReactiveObject( return createReactiveObject(
target, target,
rawToReactive, false,
reactiveToRaw,
mutableHandlers, mutableHandlers,
mutableCollectionHandlers mutableCollectionHandlers
) )
@ -60,8 +72,7 @@ export function reactive(target: object) {
export function shallowReactive<T extends object>(target: T): T { export function shallowReactive<T extends object>(target: T): T {
return createReactiveObject( return createReactiveObject(
target, target,
rawToReactive, false,
reactiveToRaw,
shallowReactiveHandlers, shallowReactiveHandlers,
mutableCollectionHandlers mutableCollectionHandlers
) )
@ -72,8 +83,7 @@ export function readonly<T extends object>(
): Readonly<UnwrapNestedRefs<T>> { ): Readonly<UnwrapNestedRefs<T>> {
return createReactiveObject( return createReactiveObject(
target, target,
rawToReadonly, true,
readonlyToRaw,
readonlyHandlers, readonlyHandlers,
readonlyCollectionHandlers readonlyCollectionHandlers
) )
@ -88,17 +98,15 @@ export function shallowReadonly<T extends object>(
): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> { ): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
return createReactiveObject( return createReactiveObject(
target, target,
rawToReadonly, true,
readonlyToRaw,
shallowReadonlyHandlers, shallowReadonlyHandlers,
readonlyCollectionHandlers readonlyCollectionHandlers
) )
} }
function createReactiveObject( function createReactiveObject(
target: unknown, target: Target,
toProxy: WeakMap<any, any>, isReadonly: boolean,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>, baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any> collectionHandlers: ProxyHandler<any>
) { ) {
@ -108,15 +116,16 @@ function createReactiveObject(
} }
return target 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 // target already has corresponding Proxy
let observed = toProxy.get(target) let observed = isReadonly ? target.__v_readonly : target.__v_reactive
if (observed !== void 0) { if (observed !== void 0) {
return observed return observed
} }
// target is already a Proxy
if (toRaw.has(target)) {
return target
}
// only a whitelist of value types can be observed. // only a whitelist of value types can be observed.
if (!canObserve(target)) { if (!canObserve(target)) {
return target return target
@ -125,30 +134,34 @@ function createReactiveObject(
? collectionHandlers ? collectionHandlers
: baseHandlers : baseHandlers
observed = new Proxy(target, handlers) observed = new Proxy(target, handlers)
toProxy.set(target, observed) def(
toRaw.set(observed, target) target,
isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive,
observed
)
return observed return observed
} }
export function isReactive(value: unknown): boolean { export function isReactive(value: unknown): boolean {
value = readonlyToRaw.get(value) || value if (isReadonly(value)) {
return reactiveToRaw.has(value) return isReactive((value as Target).__v_raw)
}
return !!(value && (value as Target).__v_isReactive)
} }
export function isReadonly(value: unknown): boolean { export function isReadonly(value: unknown): boolean {
return readonlyToRaw.has(value) return !!(value && (value as Target).__v_isReadonly)
} }
export function isProxy(value: unknown): boolean { 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 { export function toRaw<T>(observed: T): T {
observed = readonlyToRaw.get(observed) || observed return (observed && toRaw((observed as Target).__v_raw)) || observed
return reactiveToRaw.get(observed) || observed
} }
export function markRaw<T extends object>(value: T): T { export function markRaw<T extends object>(value: T): T {
rawValues.add(value) def(value, ReactiveFlags.skip, true)
return value return value
} }

View File

@ -5,18 +5,11 @@ import { reactive, isProxy, toRaw } from './reactive'
import { ComputedRef } from './computed' import { ComputedRef } from './computed'
import { CollectionTypes } from './collectionHandlers' import { CollectionTypes } from './collectionHandlers'
const isRefSymbol = Symbol()
export interface Ref<T = any> { 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. * @internal
// 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() __v_isRef: true
// 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
value: T value: T
} }
@ -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<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref { 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>( export function ref<T extends object>(
@ -51,7 +44,7 @@ function createRef(rawValue: unknown, shallow = false) {
} }
let value = shallow ? rawValue : convert(rawValue) let value = shallow ? rawValue : convert(rawValue)
const r = { const r = {
_isRef: true, __v_isRef: true,
get value() { get value() {
track(r, TrackOpTypes.GET, 'value') track(r, TrackOpTypes.GET, 'value')
return value return value
@ -99,7 +92,7 @@ export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
() => trigger(r, TriggerOpTypes.SET, 'value') () => trigger(r, TriggerOpTypes.SET, 'value')
) )
const r = { const r = {
_isRef: true, __v_isRef: true,
get value() { get value() {
return get() return get()
}, },
@ -126,7 +119,7 @@ export function toRef<T extends object, K extends keyof T>(
key: K key: K
): Ref<T[K]> { ): Ref<T[K]> {
return { return {
_isRef: true, __v_isRef: true,
get value(): any { get value(): any {
return object[key] return object[key]
}, },

View File

@ -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)
})
})

View File

@ -12,7 +12,7 @@ import {
} from '../src/vnode' } from '../src/vnode'
import { Data } from '../src/component' import { Data } from '../src/component'
import { ShapeFlags, PatchFlags } from '@vue/shared' import { ShapeFlags, PatchFlags } from '@vue/shared'
import { h } from '../src' import { h, reactive, isReactive } from '../src'
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test' import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
describe('vnode', () => { describe('vnode', () => {
@ -425,5 +425,12 @@ describe('vnode', () => {
createApp(App).mount(root) createApp(App).mount(root)
expect(serializeInner(root)).toBe('<h1>Root Component</h1>') 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)
})
}) })
}) })

View File

@ -4,8 +4,7 @@ import {
ReactiveEffect, ReactiveEffect,
pauseTracking, pauseTracking,
resetTracking, resetTracking,
shallowReadonly, shallowReadonly
markRaw
} from '@vue/reactivity' } from '@vue/reactivity'
import { import {
ComponentPublicInstance, ComponentPublicInstance,
@ -464,7 +463,7 @@ function setupStatefulComponent(
instance.accessCache = {} instance.accessCache = {}
// 1. create public instance / render proxy // 1. create public instance / render proxy
// also mark it raw so it's never observed // 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__) { if (__DEV__) {
exposePropsOnRenderContext(instance) exposePropsOnRenderContext(instance)
} }

View File

@ -6,7 +6,8 @@ import {
ReactiveEffect, ReactiveEffect,
UnwrapRef, UnwrapRef,
toRaw, toRaw,
shallowReadonly shallowReadonly,
ReactiveFlags
} from '@vue/reactivity' } from '@vue/reactivity'
import { import {
ExtractComputedReturns, ExtractComputedReturns,
@ -128,6 +129,11 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
appContext appContext
} = instance } = instance
// let @vue/reatvitiy know it should never observe Vue public instances.
if (key === ReactiveFlags.skip) {
return true
}
// data / props / ctx // data / props / ctx
// This getter gets called for every property access on the render context // This getter gets called for every property access on the render context
// during render and is a major hotspot. The most expensive part of this // during render and is a major hotspot. The most expensive part of this
@ -197,10 +203,9 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
} else if ( } else if (
__DEV__ && __DEV__ &&
currentRenderingInstance && currentRenderingInstance &&
// #1091 avoid isRef/isVNode checks on component instance leading to // #1091 avoid internal isRef/isVNode checks on component instance leading
// infinite warning loop // to infinite warning loop
key !== '_isRef' && key.indexOf('__v') !== 0
key !== '_isVNode'
) { ) {
if (data !== EMPTY_OBJ && key[0] === '$' && hasOwn(data, key)) { if (data !== EMPTY_OBJ && key[0] === '$' && hasOwn(data, key)) {
warn( warn(

View File

@ -46,7 +46,7 @@ h(Component, null, {})
type RawProps = VNodeProps & { type RawProps = VNodeProps & {
// used to differ from a single VNode object as children // used to differ from a single VNode object as children
_isVNode?: never __v_isVNode?: never
// used to differ from Array children // used to differ from Array children
[Symbol.iterator]?: never [Symbol.iterator]?: never
} }

View File

@ -103,7 +103,14 @@ export type VNodeNormalizedChildren =
| null | null
export interface VNode<HostNode = RendererNode, HostElement = RendererElement> { export interface VNode<HostNode = RendererNode, HostElement = RendererElement> {
_isVNode: true /**
* @internal
*/
__v_isVNode: true
/**
* @internal
*/
__v_skip: true
type: VNodeTypes type: VNodeTypes
props: VNodeProps | null props: VNodeProps | null
key: string | number | null key: string | number | null
@ -221,7 +228,7 @@ export function createBlock(
} }
export function isVNode(value: any): value is VNode { export function isVNode(value: any): value is VNode {
return value ? value._isVNode === true : false return value ? value.__v_isVNode === true : false
} }
export function isSameVNodeType(n1: VNode, n2: VNode): boolean { export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
@ -344,7 +351,8 @@ function _createVNode(
} }
const vnode: VNode = { const vnode: VNode = {
_isVNode: true, __v_isVNode: true,
__v_skip: true,
type, type,
props, props,
key: props && normalizeKey(props), key: props && normalizeKey(props),
@ -403,7 +411,8 @@ export function cloneVNode<T, U>(
// This is intentionally NOT using spread or extend to avoid the runtime // This is intentionally NOT using spread or extend to avoid the runtime
// key enumeration cost. // key enumeration cost.
return { return {
_isVNode: true, __v_isVNode: true,
__v_skip: true,
type: vnode.type, type: vnode.type,
props, props,
key: props && normalizeKey(props), key: props && normalizeKey(props),

View File

@ -128,5 +128,8 @@ export const invokeArrayFns = (fns: Function[], arg?: any) => {
} }
export const def = (obj: object, key: string | symbol, value: any) => { export const def = (obj: object, key: string | symbol, value: any) => {
Object.defineProperty(obj, key, { value }) Object.defineProperty(obj, key, {
configurable: true,
value
})
} }