perf(reactivity): ref-specific track/trigger and miscellaneous optimizations (#3995)
This commit is contained in:
parent
ceff89905b
commit
64310405ac
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user