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:
parent
36972c20b5
commit
d901b6bea8
@ -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'
|
||||
@ -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)
|
||||
|
@ -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 {
|
||||
@ -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(
|
||||
|
@ -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() {
|
||||
|
@ -21,7 +21,8 @@ export {
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
markRaw,
|
||||
toRaw
|
||||
toRaw,
|
||||
ReactiveFlags
|
||||
} from './reactive'
|
||||
export {
|
||||
computed,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { isObject, toRawType } from '@vue/shared'
|
||||
import { isObject, toRawType, def } from '@vue/shared'
|
||||
import {
|
||||
mutableHandlers,
|
||||
readonlyHandlers,
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
@ -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
|
||||
)
|
||||
@ -72,8 +83,7 @@ export function readonly<T extends object>(
|
||||
): Readonly<UnwrapNestedRefs<T>> {
|
||||
return createReactiveObject(
|
||||
target,
|
||||
rawToReadonly,
|
||||
readonlyToRaw,
|
||||
true,
|
||||
readonlyHandlers,
|
||||
readonlyCollectionHandlers
|
||||
)
|
||||
@ -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>
|
||||
) {
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
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(r: any): r is Ref {
|
||||
return r ? r._isRef === true : false
|
||||
return r ? r.__v_isRef === true : false
|
||||
}
|
||||
|
||||
export function ref<T extends object>(
|
||||
@ -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
|
||||
@ -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()
|
||||
},
|
||||
@ -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]
|
||||
},
|
||||
|
18
packages/runtime-core/__tests__/misc.spec.ts
Normal file
18
packages/runtime-core/__tests__/misc.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
@ -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', () => {
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -4,8 +4,7 @@ import {
|
||||
ReactiveEffect,
|
||||
pauseTracking,
|
||||
resetTracking,
|
||||
shallowReadonly,
|
||||
markRaw
|
||||
shallowReadonly
|
||||
} from '@vue/reactivity'
|
||||
import {
|
||||
ComponentPublicInstance,
|
||||
@ -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)
|
||||
}
|
||||
|
@ -6,7 +6,8 @@ import {
|
||||
ReactiveEffect,
|
||||
UnwrapRef,
|
||||
toRaw,
|
||||
shallowReadonly
|
||||
shallowReadonly,
|
||||
ReactiveFlags
|
||||
} from '@vue/reactivity'
|
||||
import {
|
||||
ExtractComputedReturns,
|
||||
@ -128,6 +129,11 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
appContext
|
||||
} = instance
|
||||
|
||||
// let @vue/reatvitiy know it should never observe Vue public instances.
|
||||
if (key === ReactiveFlags.skip) {
|
||||
return true
|
||||
}
|
||||
|
||||
// data / props / ctx
|
||||
// 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
|
||||
@ -197,10 +203,9 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
} else if (
|
||||
__DEV__ &&
|
||||
currentRenderingInstance &&
|
||||
// #1091 avoid isRef/isVNode checks on component instance leading to
|
||||
// infinite warning loop
|
||||
key !== '_isRef' &&
|
||||
key !== '_isVNode'
|
||||
// #1091 avoid internal isRef/isVNode checks on component instance leading
|
||||
// to infinite warning loop
|
||||
key.indexOf('__v') !== 0
|
||||
) {
|
||||
if (data !== EMPTY_OBJ && key[0] === '$' && hasOwn(data, key)) {
|
||||
warn(
|
||||
|
@ -46,7 +46,7 @@ h(Component, null, {})
|
||||
|
||||
type RawProps = VNodeProps & {
|
||||
// used to differ from a single VNode object as children
|
||||
_isVNode?: never
|
||||
__v_isVNode?: never
|
||||
// used to differ from Array children
|
||||
[Symbol.iterator]?: never
|
||||
}
|
||||
|
@ -103,7 +103,14 @@ export type VNodeNormalizedChildren =
|
||||
| null
|
||||
|
||||
export interface VNode<HostNode = RendererNode, HostElement = RendererElement> {
|
||||
_isVNode: true
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
__v_isVNode: true
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
__v_skip: true
|
||||
type: VNodeTypes
|
||||
props: VNodeProps | null
|
||||
key: string | number | null
|
||||
@ -221,7 +228,7 @@ export function createBlock(
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -344,7 +351,8 @@ function _createVNode(
|
||||
}
|
||||
|
||||
const vnode: VNode = {
|
||||
_isVNode: true,
|
||||
__v_isVNode: true,
|
||||
__v_skip: true,
|
||||
type,
|
||||
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
|
||||
// key enumeration cost.
|
||||
return {
|
||||
_isVNode: true,
|
||||
__v_isVNode: true,
|
||||
__v_skip: true,
|
||||
type: vnode.type,
|
||||
props,
|
||||
key: props && normalizeKey(props),
|
||||
|
@ -128,5 +128,8 @@ export const invokeArrayFns = (fns: Function[], arg?: any) => {
|
||||
}
|
||||
|
||||
export const def = (obj: object, key: string | symbol, value: any) => {
|
||||
Object.defineProperty(obj, key, { value })
|
||||
Object.defineProperty(obj, key, {
|
||||
configurable: true,
|
||||
value
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user