]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
perf(reactivity): ref-specific track/trigger and miscellaneous optimizations (#3995)
authorBas van Meurs <bvanmeurs1985@gmail.com>
Wed, 23 Jun 2021 21:22:21 +0000 (23:22 +0200)
committerEvan You <yyx990803@gmail.com>
Fri, 16 Jul 2021 18:30:49 +0000 (14:30 -0400)
packages/reactivity/src/computed.ts
packages/reactivity/src/effect.ts
packages/reactivity/src/reactive.ts
packages/reactivity/src/ref.ts

index c12f3e55d9458661b67a5a690ff1c9d8da9f2b37..965ff3eec6bb1928eae057224af9b667d988bfa3 100644 (file)
@@ -1,6 +1,5 @@
-import { effect, ReactiveEffect, trigger, track } from './effect'
-import { TriggerOpTypes, TrackOpTypes } from './operations'
-import { Ref } from './ref'
+import { effect, ReactiveEffect } from './effect'
+import { Ref, trackRefValue, triggerRefValue } from './ref'
 import { isFunction, NOOP } from '@vue/shared'
 import { ReactiveFlags, toRaw } from './reactive'
 
@@ -21,6 +20,8 @@ export interface WritableComputedOptions<T> {
 }
 
 class ComputedRefImpl<T> {
+  public dep?: Set<ReactiveEffect> = undefined
+
   private _value!: T
   private _dirty = true
 
@@ -39,7 +40,7 @@ class ComputedRefImpl<T> {
       scheduler: () => {
         if (!this._dirty) {
           this._dirty = true
-          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
+          triggerRefValue(this)
         }
       }
     })
@@ -54,7 +55,7 @@ class ComputedRefImpl<T> {
       self._value = this.effect()
       self._dirty = false
     }
-    track(self, TrackOpTypes.GET, 'value')
+    trackRefValue(this)
     return self._value
   }
 
index 0e7adb6076c20235e1b4132d9f69441b2b91de16..ed29a267c801bdfcded89daccbc89fab89b5c074 100644 (file)
@@ -46,12 +46,12 @@ export interface ReactiveEffectOptions {
 
 export type DebuggerEvent = {
   effect: ReactiveEffect
+} & DebuggerEventExtraInfo
+
+export type DebuggerEventExtraInfo = {
   target: object
   type: TrackOpTypes | TriggerOpTypes
   key: any
-} & DebuggerEventExtraInfo
-
-export interface DebuggerEventExtraInfo {
   newValue?: any
   oldValue?: any
   oldTarget?: Map<any, any> | Set<any>
@@ -111,7 +111,8 @@ function createReactiveEffect<T = any>(
       } finally {
         effectStack.pop()
         resetTracking()
-        activeEffect = effectStack[effectStack.length - 1]
+        const n = effectStack.length
+        activeEffect = n > 0 ? effectStack[n - 1] : undefined
       }
     }
   } as ReactiveEffect
@@ -154,7 +155,7 @@ export function resetTracking() {
 }
 
 export function track(target: object, type: TrackOpTypes, key: unknown) {
-  if (!shouldTrack || activeEffect === undefined) {
+  if (!isTracking()) {
     return
   }
   let depsMap = targetMap.get(target)
@@ -165,16 +166,34 @@ export function track(target: object, type: TrackOpTypes, key: unknown) {
   if (!dep) {
     depsMap.set(key, (dep = new Set()))
   }
-  if (!dep.has(activeEffect)) {
-    dep.add(activeEffect)
-    activeEffect.deps.push(dep)
-    if (__DEV__ && activeEffect.options.onTrack) {
-      activeEffect.options.onTrack({
-        effect: activeEffect,
-        target,
-        type,
-        key
-      })
+
+  const eventInfo = __DEV__
+    ? { effect: activeEffect, target, type, key }
+    : undefined
+
+  trackEffects(dep, eventInfo)
+}
+
+export function isTracking() {
+  return shouldTrack && activeEffect !== undefined
+}
+
+export function trackEffects(
+  dep: Set<ReactiveEffect>,
+  debuggerEventExtraInfo?: DebuggerEventExtraInfo
+) {
+  if (!dep.has(activeEffect!)) {
+    dep.add(activeEffect!)
+    activeEffect!.deps.push(dep)
+    if (__DEV__ && activeEffect!.options.onTrack) {
+      activeEffect!.options.onTrack(
+        Object.assign(
+          {
+            effect: activeEffect!
+          },
+          debuggerEventExtraInfo
+        )
+      )
     }
   }
 }
@@ -193,73 +212,88 @@ export function trigger(
     return
   }
 
-  const effects = new Set<ReactiveEffect>()
-  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
-    if (effectsToAdd) {
-      effectsToAdd.forEach(effect => {
-        if (effect !== activeEffect || effect.allowRecurse) {
-          effects.add(effect)
-        }
-      })
-    }
-  }
-
+  let sets: DepSets = []
   if (type === TriggerOpTypes.CLEAR) {
     // collection being cleared
     // trigger all effects for target
-    depsMap.forEach(add)
+    sets = [...depsMap.values()]
   } else if (key === 'length' && isArray(target)) {
     depsMap.forEach((dep, key) => {
       if (key === 'length' || key >= (newValue as number)) {
-        add(dep)
+        sets.push(dep)
       }
     })
   } else {
     // schedule runs for SET | ADD | DELETE
     if (key !== void 0) {
-      add(depsMap.get(key))
+      sets.push(depsMap.get(key))
     }
 
     // also run for iteration key on ADD | DELETE | Map.SET
     switch (type) {
       case TriggerOpTypes.ADD:
         if (!isArray(target)) {
-          add(depsMap.get(ITERATE_KEY))
+          sets.push(depsMap.get(ITERATE_KEY))
           if (isMap(target)) {
-            add(depsMap.get(MAP_KEY_ITERATE_KEY))
+            sets.push(depsMap.get(MAP_KEY_ITERATE_KEY))
           }
         } else if (isIntegerKey(key)) {
           // new index added to array -> length changes
-          add(depsMap.get('length'))
+          sets.push(depsMap.get('length'))
         }
         break
       case TriggerOpTypes.DELETE:
         if (!isArray(target)) {
-          add(depsMap.get(ITERATE_KEY))
+          sets.push(depsMap.get(ITERATE_KEY))
           if (isMap(target)) {
-            add(depsMap.get(MAP_KEY_ITERATE_KEY))
+            sets.push(depsMap.get(MAP_KEY_ITERATE_KEY))
           }
         }
         break
       case TriggerOpTypes.SET:
         if (isMap(target)) {
-          add(depsMap.get(ITERATE_KEY))
+          sets.push(depsMap.get(ITERATE_KEY))
         }
         break
     }
   }
 
+  const eventInfo = __DEV__
+    ? { target, type, key, newValue, oldValue, oldTarget }
+    : undefined
+  triggerMultiEffects(sets, eventInfo)
+}
+
+type DepSets = (Dep | undefined)[]
+
+export function triggerMultiEffects(
+  depSets: DepSets,
+  debuggerEventExtraInfo?: DebuggerEventExtraInfo
+) {
+  if (depSets.length === 1) {
+    if (depSets[0]) {
+      triggerEffects(depSets[0], debuggerEventExtraInfo)
+    }
+  } else {
+    const sets = depSets.filter(s => !!s) as Dep[]
+    triggerEffects(concatSets(sets), debuggerEventExtraInfo)
+  }
+}
+
+function concatSets<T>(sets: Set<T>[]): Set<T> {
+  const all = ([] as T[]).concat(...sets.map(s => [...s!]))
+  return new Set(all)
+}
+
+export function triggerEffects(
+  dep: Dep,
+  debuggerEventExtraInfo?: DebuggerEventExtraInfo
+) {
   const run = (effect: ReactiveEffect) => {
     if (__DEV__ && effect.options.onTrigger) {
-      effect.options.onTrigger({
-        effect,
-        target,
-        key,
-        type,
-        newValue,
-        oldValue,
-        oldTarget
-      })
+      effect.options.onTrigger(
+        Object.assign({ effect }, debuggerEventExtraInfo)
+      )
     }
     if (effect.options.scheduler) {
       effect.options.scheduler(effect)
@@ -268,5 +302,10 @@ export function trigger(
     }
   }
 
-  effects.forEach(run)
+  const immutableDeps = [...dep]
+  immutableDeps.forEach(effect => {
+    if (effect !== activeEffect || effect.allowRecurse) {
+      run(effect)
+    }
+  })
 }
index ea800c83c61f5eeda1860c21e3c2df2f89635c1d..2137e7cfb67aa0f74e69f441a5c8f00c3c4d16ce 100644 (file)
@@ -225,9 +225,8 @@ export function isProxy(value: unknown): boolean {
 }
 
 export function toRaw<T>(observed: T): T {
-  return (
-    (observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed
-  )
+  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
+  return raw ? toRaw(raw) : observed
 }
 
 export function markRaw<T extends object>(value: T): T {
index 1f6c93cacfac3d043339206b79247d8fcf179849..3d7e2388a565797da42e0b53673f93e08796c112 100644 (file)
@@ -1,4 +1,9 @@
-import { track, trigger } from './effect'
+import {
+  isTracking,
+  ReactiveEffect,
+  trackEffects,
+  triggerEffects
+} from './effect'
 import { TrackOpTypes, TriggerOpTypes } from './operations'
 import { isArray, isObject, hasChanged } from '@vue/shared'
 import { reactive, isProxy, toRaw, isReactive } from './reactive'
@@ -18,6 +23,44 @@ export interface Ref<T = any> {
    * @internal
    */
   _shallow?: boolean
+
+  /**
+   * Deps are maintained locally rather than in depsMap for performance reasons.
+   */
+  dep?: Set<ReactiveEffect>
+}
+
+type RefBase<T> = {
+  dep?: Set<ReactiveEffect>
+  value: T
+}
+
+export function trackRefValue(ref: RefBase<any>) {
+  if (isTracking()) {
+    ref = toRaw(ref)
+    const eventInfo = __DEV__
+      ? { target: ref, type: TrackOpTypes.GET, key: 'value' }
+      : undefined
+    if (!ref.dep) {
+      ref.dep = new Set<ReactiveEffect>()
+    }
+    trackEffects(ref.dep, eventInfo)
+  }
+}
+
+export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
+  ref = toRaw(ref)
+  if (ref.dep) {
+    const eventInfo = __DEV__
+      ? {
+          target: ref,
+          type: TriggerOpTypes.SET,
+          key: 'value',
+          newValue: newVal
+        }
+      : undefined
+    triggerEffects(ref.dep, eventInfo)
+  }
 }
 
 export type ToRef<T> = [T] extends [Ref] ? T : Ref<UnwrapRef<T>>
@@ -52,10 +95,10 @@ export function shallowRef(value?: unknown) {
 }
 
 class RefImpl<T> {
-  private _rawValue: T
-
   private _value: T
+  private _rawValue: T
 
+  public dep?: Set<ReactiveEffect> = undefined
   public readonly __v_isRef = true
 
   constructor(value: T, public readonly _shallow = false) {
@@ -64,7 +107,7 @@ class RefImpl<T> {
   }
 
   get value() {
-    track(toRaw(this), TrackOpTypes.GET, 'value')
+    trackRefValue(this)
     return this._value
   }
 
@@ -73,7 +116,7 @@ class RefImpl<T> {
     if (hasChanged(newVal, this._rawValue)) {
       this._rawValue = newVal
       this._value = this._shallow ? newVal : convert(newVal)
-      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
+      triggerRefValue(this, newVal)
     }
   }
 }
@@ -86,7 +129,7 @@ function createRef(rawValue: unknown, shallow = false) {
 }
 
 export function triggerRef(ref: Ref) {
-  trigger(toRaw(ref), TriggerOpTypes.SET, 'value', __DEV__ ? ref.value : void 0)
+  triggerRefValue(ref, __DEV__ ? ref.value : void 0)
 }
 
 export function unref<T>(ref: T | Ref<T>): T {
@@ -123,6 +166,8 @@ export type CustomRefFactory<T> = (
 }
 
 class CustomRefImpl<T> {
+  public dep?: Set<ReactiveEffect> = undefined
+
   private readonly _get: ReturnType<CustomRefFactory<T>>['get']
   private readonly _set: ReturnType<CustomRefFactory<T>>['set']
 
@@ -130,8 +175,8 @@ class CustomRefImpl<T> {
 
   constructor(factory: CustomRefFactory<T>) {
     const { get, set } = factory(
-      () => track(this, TrackOpTypes.GET, 'value'),
-      () => trigger(this, TriggerOpTypes.SET, 'value')
+      () => trackRefValue(this),
+      () => triggerRefValue(this)
     )
     this._get = get
     this._set = set