Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions packages/reactivity/src/computed.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -21,6 +20,8 @@ export interface WritableComputedOptions<T> {
}

class ComputedRefImpl<T> {
public dep?: Set<ReactiveEffect> = undefined

private _value!: T
private _dirty = true

Expand All @@ -39,7 +40,7 @@ class ComputedRefImpl<T> {
scheduler: () => {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')
triggerRefValue(this)
}
}
})
Expand All @@ -54,7 +55,7 @@ class ComputedRefImpl<T> {
self._value = this.effect()
self._dirty = false
}
track(self, TrackOpTypes.GET, 'value')
trackRefValue(this)
return self._value
}

Expand Down
129 changes: 84 additions & 45 deletions packages/reactivity/src/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
)
)
}
}
}
Expand All @@ -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)
Expand All @@ -268,5 +302,10 @@ export function trigger(
}
}

effects.forEach(run)
const immutableDeps = [...dep]
immutableDeps.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
run(effect)
}
})
}
5 changes: 2 additions & 3 deletions packages/reactivity/src/reactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
59 changes: 53 additions & 6 deletions packages/reactivity/src/ref.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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>>
Expand Down Expand Up @@ -52,6 +95,8 @@ export function shallowRef(value?: unknown) {
}

class RefImpl<T> {
public dep?: Set<ReactiveEffect> = undefined

private _value: T

public readonly __v_isRef = true
Expand All @@ -61,15 +106,15 @@ class RefImpl<T> {
}

get value() {
track(toRaw(this), TrackOpTypes.GET, 'value')
trackRefValue(this)
return this._value
}

set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
triggerRefValue(this, newVal)
}
}
}
Expand All @@ -82,7 +127,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): T extends Ref<infer V> ? V : T {
Expand Down Expand Up @@ -119,15 +164,17 @@ 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']

public readonly __v_isRef = true

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
Expand Down