]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(reactivity): use more efficient reactive checks
authorEvan You <yyx990803@gmail.com>
Sat, 2 May 2020 20:16:51 +0000 (16:16 -0400)
committerEvan You <yyx990803@gmail.com>
Sat, 2 May 2020 20:58:17 +0000 (16:58 -0400)
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.

13 files changed:
packages/reactivity/src/baseHandlers.ts
packages/reactivity/src/collectionHandlers.ts
packages/reactivity/src/computed.ts
packages/reactivity/src/index.ts
packages/reactivity/src/reactive.ts
packages/reactivity/src/ref.ts
packages/runtime-core/__tests__/misc.spec.ts [new file with mode: 0644]
packages/runtime-core/__tests__/vnode.spec.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/h.ts
packages/runtime-core/src/vnode.ts
packages/shared/src/index.ts

index 5fc10b02716fe1217229694853e9f913b82362c9..531d19c7de276a53ea3b147f1304462a1a513ad4 100644 (file)
@@ -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)
index 6ab7e6fdd681b94a593220e3900040146da1e87f..b03cbd589ae2249f82136a965cdf89366cb5d022 100644 (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 { 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(
index 154247a672c95186121b7707c46dcfdd38070af7..d6f89fe284ecdb24db4c1b20c02c38c073607944 100644 (file)
@@ -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() {
index 1c9cf821834cd019cab9f3f23ef649e440c4e0a8..bbab3118a837a0077634e7abd2abd8d37c8aaacd 100644 (file)
@@ -21,7 +21,8 @@ export {
   shallowReactive,
   shallowReadonly,
   markRaw,
-  toRaw
+  toRaw,
+  ReactiveFlags
 } from './reactive'
 export {
   computed,
index 7d7e2558fa2705ba71282e2b6a6146a45def1128..6d48b6c0a8c90dd09af3c3a646c1b45ccdd6b12e 100644 (file)
@@ -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
 }
index 07a5fc85d28c022a1fba62bb85de9425baccf0a0..629a2dad9b0a63e2cfa583a81c1718293d0c7650 100644 (file)
@@ -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]
     },
diff --git a/packages/runtime-core/__tests__/misc.spec.ts b/packages/runtime-core/__tests__/misc.spec.ts
new file mode 100644 (file)
index 0000000..4cb93eb
--- /dev/null
@@ -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)
+  })
+})
index 2d8dd80410b7191dd3fc5317303aa41adbf18fca..e8bde9bcc8333d2d6a00ef530b2c352a27ad6977 100644 (file)
@@ -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)
+    })
   })
 })
index 1c9b17baeec47b62909011f69765fe7668f3dd2a..a26837bc869123a0b47fbaee134d444a72edbbc3 100644 (file)
@@ -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)
   }
index 71a056e85990004f8ef6162ac9343a14bc7a8dab..ff895a619690a043bb95777364446e3dd6b8a219 100644 (file)
@@ -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(
index aebd8506f5d05d2a0669f951297885849f1acde6..4e644c3e05510b8d28f193aacae725beb3c8fecb 100644 (file)
@@ -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
 }
index 508f0136bb1aa3250effcb888ebb948453f8c87a..e4107da78d8ed67065d3090040dd073487173a38 100644 (file)
@@ -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),
index 8274c10de875648bdcfb8b585a201665394893e6..c679dfe6d11033ddd0d0c267bd1b3a87e133d4d9 100644 (file)
@@ -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
+  })
 }