perf(reactivity): ref-specific track/trigger and miscellaneous optimizations (#3995)

This commit is contained in:
Bas van Meurs 2021-06-23 23:22:21 +02:00 committed by Evan You
parent ceff89905b
commit 64310405ac
4 changed files with 145 additions and 61 deletions

View File

@ -1,6 +1,5 @@
import { effect, ReactiveEffect, trigger, track } from './effect' import { effect, ReactiveEffect } from './effect'
import { TriggerOpTypes, TrackOpTypes } from './operations' import { Ref, trackRefValue, triggerRefValue } from './ref'
import { Ref } from './ref'
import { isFunction, NOOP } from '@vue/shared' import { isFunction, NOOP } from '@vue/shared'
import { ReactiveFlags, toRaw } from './reactive' import { ReactiveFlags, toRaw } from './reactive'
@ -21,6 +20,8 @@ export interface WritableComputedOptions<T> {
} }
class ComputedRefImpl<T> { class ComputedRefImpl<T> {
public dep?: Set<ReactiveEffect> = undefined
private _value!: T private _value!: T
private _dirty = true private _dirty = true
@ -39,7 +40,7 @@ class ComputedRefImpl<T> {
scheduler: () => { scheduler: () => {
if (!this._dirty) { if (!this._dirty) {
this._dirty = true this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value') triggerRefValue(this)
} }
} }
}) })
@ -54,7 +55,7 @@ class ComputedRefImpl<T> {
self._value = this.effect() self._value = this.effect()
self._dirty = false self._dirty = false
} }
track(self, TrackOpTypes.GET, 'value') trackRefValue(this)
return self._value return self._value
} }

View File

@ -46,12 +46,12 @@ export interface ReactiveEffectOptions {
export type DebuggerEvent = { export type DebuggerEvent = {
effect: ReactiveEffect effect: ReactiveEffect
} & DebuggerEventExtraInfo
export type DebuggerEventExtraInfo = {
target: object target: object
type: TrackOpTypes | TriggerOpTypes type: TrackOpTypes | TriggerOpTypes
key: any key: any
} & DebuggerEventExtraInfo
export interface DebuggerEventExtraInfo {
newValue?: any newValue?: any
oldValue?: any oldValue?: any
oldTarget?: Map<any, any> | Set<any> oldTarget?: Map<any, any> | Set<any>
@ -111,7 +111,8 @@ function createReactiveEffect<T = any>(
} finally { } finally {
effectStack.pop() effectStack.pop()
resetTracking() resetTracking()
activeEffect = effectStack[effectStack.length - 1] const n = effectStack.length
activeEffect = n > 0 ? effectStack[n - 1] : undefined
} }
} }
} as ReactiveEffect } as ReactiveEffect
@ -154,7 +155,7 @@ export function resetTracking() {
} }
export function track(target: object, type: TrackOpTypes, key: unknown) { export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) { if (!isTracking()) {
return return
} }
let depsMap = targetMap.get(target) let depsMap = targetMap.get(target)
@ -165,16 +166,34 @@ export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!dep) { if (!dep) {
depsMap.set(key, (dep = new Set())) depsMap.set(key, (dep = new Set()))
} }
if (!dep.has(activeEffect)) {
dep.add(activeEffect) const eventInfo = __DEV__
activeEffect.deps.push(dep) ? { effect: activeEffect, target, type, key }
if (__DEV__ && activeEffect.options.onTrack) { : undefined
activeEffect.options.onTrack({
effect: activeEffect, trackEffects(dep, eventInfo)
target, }
type,
key 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 return
} }
const effects = new Set<ReactiveEffect>() let sets: DepSets = []
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect)
}
})
}
}
if (type === TriggerOpTypes.CLEAR) { if (type === TriggerOpTypes.CLEAR) {
// collection being cleared // collection being cleared
// trigger all effects for target // trigger all effects for target
depsMap.forEach(add) sets = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) { } else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => { depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) { if (key === 'length' || key >= (newValue as number)) {
add(dep) sets.push(dep)
} }
}) })
} else { } else {
// schedule runs for SET | ADD | DELETE // schedule runs for SET | ADD | DELETE
if (key !== void 0) { if (key !== void 0) {
add(depsMap.get(key)) sets.push(depsMap.get(key))
} }
// also run for iteration key on ADD | DELETE | Map.SET // also run for iteration key on ADD | DELETE | Map.SET
switch (type) { switch (type) {
case TriggerOpTypes.ADD: case TriggerOpTypes.ADD:
if (!isArray(target)) { if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY)) sets.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) { if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY)) sets.push(depsMap.get(MAP_KEY_ITERATE_KEY))
} }
} else if (isIntegerKey(key)) { } else if (isIntegerKey(key)) {
// new index added to array -> length changes // new index added to array -> length changes
add(depsMap.get('length')) sets.push(depsMap.get('length'))
} }
break break
case TriggerOpTypes.DELETE: case TriggerOpTypes.DELETE:
if (!isArray(target)) { if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY)) sets.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) { if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY)) sets.push(depsMap.get(MAP_KEY_ITERATE_KEY))
} }
} }
break break
case TriggerOpTypes.SET: case TriggerOpTypes.SET:
if (isMap(target)) { if (isMap(target)) {
add(depsMap.get(ITERATE_KEY)) sets.push(depsMap.get(ITERATE_KEY))
} }
break 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) => { const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) { if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({ effect.options.onTrigger(
effect, Object.assign({ effect }, debuggerEventExtraInfo)
target, )
key,
type,
newValue,
oldValue,
oldTarget
})
} }
if (effect.options.scheduler) { if (effect.options.scheduler) {
effect.options.scheduler(effect) 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)
}
})
} }

View File

@ -225,9 +225,8 @@ export function isProxy(value: unknown): boolean {
} }
export function toRaw<T>(observed: T): T { export function toRaw<T>(observed: T): T {
return ( const raw = observed && (observed as Target)[ReactiveFlags.RAW]
(observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed return raw ? toRaw(raw) : observed
)
} }
export function markRaw<T extends object>(value: T): T { export function markRaw<T extends object>(value: T): T {

View File

@ -1,4 +1,9 @@
import { track, trigger } from './effect' import {
isTracking,
ReactiveEffect,
trackEffects,
triggerEffects
} from './effect'
import { TrackOpTypes, TriggerOpTypes } from './operations' import { TrackOpTypes, TriggerOpTypes } from './operations'
import { isArray, isObject, hasChanged } from '@vue/shared' import { isArray, isObject, hasChanged } from '@vue/shared'
import { reactive, isProxy, toRaw, isReactive } from './reactive' import { reactive, isProxy, toRaw, isReactive } from './reactive'
@ -18,6 +23,44 @@ export interface Ref<T = any> {
* @internal * @internal
*/ */
_shallow?: boolean _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>> export type ToRef<T> = [T] extends [Ref] ? T : Ref<UnwrapRef<T>>
@ -52,10 +95,10 @@ export function shallowRef(value?: unknown) {
} }
class RefImpl<T> { class RefImpl<T> {
private _value: T
private _rawValue: T private _rawValue: T
private _value: T public dep?: Set<ReactiveEffect> = undefined
public readonly __v_isRef = true public readonly __v_isRef = true
constructor(value: T, public readonly _shallow = false) { constructor(value: T, public readonly _shallow = false) {
@ -64,7 +107,7 @@ class RefImpl<T> {
} }
get value() { get value() {
track(toRaw(this), TrackOpTypes.GET, 'value') trackRefValue(this)
return this._value return this._value
} }
@ -73,7 +116,7 @@ class RefImpl<T> {
if (hasChanged(newVal, this._rawValue)) { if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal this._rawValue = newVal
this._value = this._shallow ? newVal : convert(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) { 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 { export function unref<T>(ref: T | Ref<T>): T {
@ -123,6 +166,8 @@ export type CustomRefFactory<T> = (
} }
class CustomRefImpl<T> { class CustomRefImpl<T> {
public dep?: Set<ReactiveEffect> = undefined
private readonly _get: ReturnType<CustomRefFactory<T>>['get'] private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set'] private readonly _set: ReturnType<CustomRefFactory<T>>['set']
@ -130,8 +175,8 @@ class CustomRefImpl<T> {
constructor(factory: CustomRefFactory<T>) { constructor(factory: CustomRefFactory<T>) {
const { get, set } = factory( const { get, set } = factory(
() => track(this, TrackOpTypes.GET, 'value'), () => trackRefValue(this),
() => trigger(this, TriggerOpTypes.SET, 'value') () => triggerRefValue(this)
) )
this._get = get this._get = get
this._set = set this._set = set