From 64310405acaccabc24985ade95fb1b5c9c06ef76 Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Wed, 23 Jun 2021 23:22:21 +0200 Subject: [PATCH] perf(reactivity): ref-specific track/trigger and miscellaneous optimizations (#3995) --- packages/reactivity/src/computed.ts | 11 +-- packages/reactivity/src/effect.ts | 129 ++++++++++++++++++---------- packages/reactivity/src/reactive.ts | 5 +- packages/reactivity/src/ref.ts | 61 +++++++++++-- 4 files changed, 145 insertions(+), 61 deletions(-) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index c12f3e55..965ff3ee 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -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 { } class ComputedRefImpl { + public dep?: Set = undefined + private _value!: T private _dirty = true @@ -39,7 +40,7 @@ class ComputedRefImpl { scheduler: () => { if (!this._dirty) { this._dirty = true - trigger(toRaw(this), TriggerOpTypes.SET, 'value') + triggerRefValue(this) } } }) @@ -54,7 +55,7 @@ class ComputedRefImpl { self._value = this.effect() self._dirty = false } - track(self, TrackOpTypes.GET, 'value') + trackRefValue(this) return self._value } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 0e7adb60..ed29a267 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -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 | Set @@ -111,7 +111,8 @@ function createReactiveEffect( } 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, + 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() - const add = (effectsToAdd: Set | 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(sets: Set[]): Set { + 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) + } + }) } diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index ea800c83..2137e7cf 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -225,9 +225,8 @@ export function isProxy(value: unknown): boolean { } export function toRaw(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(value: T): T { diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 1f6c93ca..3d7e2388 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -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 { * @internal */ _shallow?: boolean + + /** + * Deps are maintained locally rather than in depsMap for performance reasons. + */ + dep?: Set +} + +type RefBase = { + dep?: Set + value: T +} + +export function trackRefValue(ref: RefBase) { + if (isTracking()) { + ref = toRaw(ref) + const eventInfo = __DEV__ + ? { target: ref, type: TrackOpTypes.GET, key: 'value' } + : undefined + if (!ref.dep) { + ref.dep = new Set() + } + trackEffects(ref.dep, eventInfo) + } +} + +export function triggerRefValue(ref: RefBase, 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] extends [Ref] ? T : Ref> @@ -52,10 +95,10 @@ export function shallowRef(value?: unknown) { } class RefImpl { + private _value: T private _rawValue: T - private _value: T - + public dep?: Set = undefined public readonly __v_isRef = true constructor(value: T, public readonly _shallow = false) { @@ -64,7 +107,7 @@ class RefImpl { } get value() { - track(toRaw(this), TrackOpTypes.GET, 'value') + trackRefValue(this) return this._value } @@ -73,7 +116,7 @@ class RefImpl { 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(ref: T | Ref): T { @@ -123,6 +166,8 @@ export type CustomRefFactory = ( } class CustomRefImpl { + public dep?: Set = undefined + private readonly _get: ReturnType>['get'] private readonly _set: ReturnType>['set'] @@ -130,8 +175,8 @@ class CustomRefImpl { constructor(factory: CustomRefFactory) { const { get, set } = factory( - () => track(this, TrackOpTypes.GET, 'value'), - () => trigger(this, TriggerOpTypes.SET, 'value') + () => trackRefValue(this), + () => triggerRefValue(this) ) this._get = get this._set = set