diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 5fc10b02..531d19c7 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -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 = {} 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) diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index 6ab7e6fd..b03cbd58 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -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 -) { +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 = { - get: createInstrumentationGetter(mutableInstrumentations) + get: createInstrumentationGetter(false) } export const readonlyCollectionHandlers: ProxyHandler = { - get: createInstrumentationGetter(readonlyInstrumentations) + get: createInstrumentationGetter(true) } function checkIdentityKeys( diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 154247a6..d6f89fe2 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -57,7 +57,7 @@ export function computed( } }) computed = { - _isRef: true, + __v_isRef: true, // expose effect so computed can be stopped effect: runner, get value() { diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 1c9cf821..bbab3118 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -21,7 +21,8 @@ export { shallowReactive, shallowReadonly, markRaw, - toRaw + toRaw, + ReactiveFlags } from './reactive' export { computed, diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index 7d7e2558..6d48b6c0 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -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() -const reactiveToRaw = new WeakMap() -const rawToReadonly = new WeakMap() -const readonlyToRaw = new WeakMap() +// const rawToReactive = new WeakMap() +// const reactiveToRaw = new WeakMap() +// const rawToReadonly = new WeakMap() +// const readonlyToRaw = new WeakMap() -// WeakSets for values that are marked readonly or non-reactive during -// observable creation. -const rawValues = new WeakSet() +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([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 extends Ref ? T : UnwrapRef export function reactive(target: T): UnwrapNestedRefs 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(target: T): T { return createReactiveObject( target, - rawToReactive, - reactiveToRaw, + false, shallowReactiveHandlers, mutableCollectionHandlers ) @@ -72,8 +83,7 @@ export function readonly( ): Readonly> { return createReactiveObject( target, - rawToReadonly, - readonlyToRaw, + true, readonlyHandlers, readonlyCollectionHandlers ) @@ -88,17 +98,15 @@ export function shallowReadonly( ): Readonly<{ [K in keyof T]: UnwrapNestedRefs }> { return createReactiveObject( target, - rawToReadonly, - readonlyToRaw, + true, shallowReadonlyHandlers, readonlyCollectionHandlers ) } function createReactiveObject( - target: unknown, - toProxy: WeakMap, - toRaw: WeakMap, + target: Target, + isReadonly: boolean, baseHandlers: ProxyHandler, collectionHandlers: ProxyHandler ) { @@ -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(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(value: T): T { - rawValues.add(value) + def(value, ReactiveFlags.skip, true) return value } diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 07a5fc85..629a2dad 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -5,18 +5,11 @@ import { reactive, isProxy, toRaw } from './reactive' import { ComputedRef } from './computed' import { CollectionTypes } from './collectionHandlers' -const isRefSymbol = Symbol() - export interface Ref { - // 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 = (val: T): T => export function isRef(r: Ref | unknown): 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( @@ -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(factory: CustomRefFactory): Ref { () => trigger(r, TriggerOpTypes.SET, 'value') ) const r = { - _isRef: true, + __v_isRef: true, get value() { return get() }, @@ -126,7 +119,7 @@ export function toRef( key: K ): Ref { return { - _isRef: true, + __v_isRef: true, get value(): any { return object[key] }, diff --git a/packages/runtime-core/__tests__/misc.spec.ts b/packages/runtime-core/__tests__/misc.spec.ts new file mode 100644 index 00000000..4cb93eb4 --- /dev/null +++ b/packages/runtime-core/__tests__/misc.spec.ts @@ -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) + }) +}) diff --git a/packages/runtime-core/__tests__/vnode.spec.ts b/packages/runtime-core/__tests__/vnode.spec.ts index 2d8dd804..e8bde9bc 100644 --- a/packages/runtime-core/__tests__/vnode.spec.ts +++ b/packages/runtime-core/__tests__/vnode.spec.ts @@ -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('

Root Component

') }) + + test('should not be observable', () => { + const a = createVNode('div') + const b = reactive(a) + expect(b).toBe(a) + expect(isReactive(b)).toBe(false) + }) }) }) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 1c9b17ba..a26837bc 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -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) } diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 71a056e8..ff895a61 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -6,7 +6,8 @@ import { ReactiveEffect, UnwrapRef, toRaw, - shallowReadonly + shallowReadonly, + ReactiveFlags } from '@vue/reactivity' import { ExtractComputedReturns, @@ -128,6 +129,11 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { 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 = { } 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( diff --git a/packages/runtime-core/src/h.ts b/packages/runtime-core/src/h.ts index aebd8506..4e644c3e 100644 --- a/packages/runtime-core/src/h.ts +++ b/packages/runtime-core/src/h.ts @@ -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 } diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 508f0136..e4107da7 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -103,7 +103,14 @@ export type VNodeNormalizedChildren = | null export interface VNode { - _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( // 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), diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 8274c10d..c679dfe6 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -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 + }) }