Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f485ea0
perf: calculate embedded `computed()` on-demand
johnsoncodehk May 13, 2022
208e59c
fix: computed effect dons't transfer
johnsoncodehk May 13, 2022
4a3e554
fix: revert effects trigger order
johnsoncodehk May 13, 2022
ed871c2
refactor: remove `computedToAskDirty` arg from `trigger()`
johnsoncodehk May 13, 2022
fbf214a
chore: use `hasChanged()` instead of `!==`
johnsoncodehk May 13, 2022
399f155
perf: reduce `triggerRefValue()` triggered
johnsoncodehk May 14, 2022
0e0c44e
perf: avoid duplicate trigger effect to deps
johnsoncodehk Jan 11, 2023
7768d43
fix: avoid track unrelated effects
johnsoncodehk Feb 2, 2023
a353eac
chore: make `_computedsToAskDirty` private
johnsoncodehk Feb 2, 2023
014dcd5
fix: jest -> vitest
johnsoncodehk Feb 2, 2023
eb0f8fa
fix: urgent assessment edge case
johnsoncodehk Apr 5, 2023
f3a0071
feat: more purposeful test
johnsoncodehk Apr 5, 2023
998c400
chore: add test for effect() on-demand trigger
johnsoncodehk Apr 6, 2023
b5527a1
Revert "chore: add test for effect() on-demand trigger"
johnsoncodehk Apr 6, 2023
3e0c2af
wip: on-demand effect()
johnsoncodehk Apr 6, 2023
62dc5ee
fix: ref newVal arg incorrect
johnsoncodehk Apr 7, 2023
e34619f
Merge branch 'computed-ondemand' into effect-ondemand
johnsoncodehk Apr 7, 2023
8895117
release: v3.3.0-alpha.9
haoqunjiang Apr 7, 2023
909afcc
fix: fixed effect for tests
johnsoncodehk Apr 7, 2023
da6263c
refactor: implement simplify
johnsoncodehk Apr 7, 2023
d2fd264
Revert "release: v3.3.0-alpha.9"
haoqunjiang Apr 7, 2023
f4d345d
Merge remote-tracking branch 'upstream/main' into computed-ondemand
johnsoncodehk Apr 8, 2023
866a3ee
Merge branch 'computed-ondemand' into effect-ondemand
johnsoncodehk Apr 8, 2023
0a16e82
Merge branch 'main' into pr/5912
johnsoncodehk Jul 31, 2023
6c9f058
chore: naming
johnsoncodehk Jul 31, 2023
66bcc3e
chore: with "s"
johnsoncodehk Jul 31, 2023
06f813e
Merge branch 'pr/5912' into pr/8043
johnsoncodehk Jul 31, 2023
debddde
chore: naming
johnsoncodehk Jul 31, 2023
ba66880
chore: undo format
johnsoncodehk Jul 31, 2023
cb081f6
chore: `_c` -> `deferredComputed`
johnsoncodehk Jul 31, 2023
7d21edf
Merge branch 'pr/5912' into pr/8043
johnsoncodehk Jul 31, 2023
76e2e6e
chore: `_c` -> `deferredComputed`
johnsoncodehk Jul 31, 2023
de4e436
chore: expressiveness
johnsoncodehk Jul 31, 2023
6c13a30
refactor: reuse `effectTrackDepth`
johnsoncodehk Jul 31, 2023
ef2a6e3
refactor: remove unneeded `pauseTracking()`, `resetTracking()`
johnsoncodehk Jul 31, 2023
7bc1069
chore: sort in one line
johnsoncodehk Jul 31, 2023
d77f842
Merge branch 'pr/5912' into pr/8043
johnsoncodehk Jul 31, 2023
f0512d6
chore: sort in one line
johnsoncodehk Jul 31, 2023
7e2ac74
chore: naming
johnsoncodehk Jul 31, 2023
e98d329
chore: import type
johnsoncodehk Jul 31, 2023
02805c5
Merge branch 'pr/5912' into pr/8043
johnsoncodehk Jul 31, 2023
d5595cb
chore: narrow `pauseTracking()`
johnsoncodehk Aug 1, 2023
01c7b80
Merge branch 'pr/5912' into pr/8043
johnsoncodehk Aug 1, 2023
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
71 changes: 71 additions & 0 deletions packages/reactivity/__tests__/computed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,75 @@ describe('reactivity/computed', () => {
oldValue: 2
})
})

it('chained computed value on-demand trigger', () => {
const minSpy = vi.fn()
const hourSpy = vi.fn()

const sec = ref(0)
const min = computed(() => {
minSpy()
return Math.floor(sec.value / 60)
})
const hour = computed(() => {
hourSpy()
return Math.floor(min.value / 60)
})

for (sec.value = 0; sec.value < 1000; sec.value++) {
hour.value
}

expect(minSpy).toHaveBeenCalledTimes(1000)
expect(hourSpy).toHaveBeenCalledTimes(17)
})

it('effect callback on-demand trigger', () => {
const effectSpy = vi.fn()

const sec = ref(0)
const min = computed(() => {
return Math.floor(sec.value / 60)
})
const hour = computed(() => {
return Math.floor(min.value / 60)
})

effect(() => {
effectSpy()
min.value
hour.value
})

for (sec.value = 0; sec.value < 1000; sec.value++) {}

expect(effectSpy).toHaveBeenCalledTimes(17)
})

it('chained computed value urgent assessment edge case', () => {
const cSpy = vi.fn()

const a = ref<null | { v: number }>({
v: 1
})
const b = computed(() => {
return a.value
})
const c = computed(() => {
cSpy()
return b.value?.v
})
const d = computed(() => {
if (b.value) {
return c.value
}
return 0
})

d.value
a.value!.v = 2
a.value = null
d.value
expect(cSpy).toHaveBeenCalledTimes(1)
})
})
48 changes: 42 additions & 6 deletions packages/reactivity/src/computed.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { DebuggerOptions, ReactiveEffect } from './effect'
import {
DebuggerOptions,
pauseTracking,
ReactiveEffect,
resetTracking
} from './effect'
import { Ref, trackRefValue, triggerRefValue } from './ref'
import { isFunction, NOOP } from '@vue/shared'
import { hasChanged, isFunction, NOOP } from '@vue/shared'
import { ReactiveFlags, toRaw } from './reactive'
import { Dep } from './dep'

Expand Down Expand Up @@ -33,6 +38,8 @@ export class ComputedRefImpl<T> {
public readonly [ReactiveFlags.IS_READONLY]: boolean = false

public _dirty = true
public _scheduled = false
public _deferredComputeds: ComputedRefImpl<any>[] = []
public _cacheable: boolean

constructor(
Expand All @@ -41,10 +48,17 @@ export class ComputedRefImpl<T> {
isReadonly: boolean,
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
this.effect = new ReactiveEffect(getter, deferredComputed => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
if (deferredComputed) {
this._deferredComputeds.push(deferredComputed)
} else {
this._dirty = true
}
if (!this._scheduled) {
this._scheduled = true
triggerRefValue(this, this)
}
}
})
this.effect.computed = this
Expand All @@ -55,11 +69,33 @@ export class ComputedRefImpl<T> {
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
if (!self._dirty && self._deferredComputeds.length) {
if (self._deferredComputeds.length >= 2) {
self._deferredComputeds = self._deferredComputeds.sort(
(a, b) =>
self.effect.deps.indexOf(a.dep!) - self.effect.deps.indexOf(b.dep!)
)
}
pauseTracking()
for (const deferredComputed of self._deferredComputeds) {
deferredComputed.value
if (self._dirty) {
break
}
}
resetTracking()
}
trackRefValue(self)
if (self._dirty || !self._cacheable) {
const newValue = self.effect.run()!
if (hasChanged(self._value, newValue)) {
triggerRefValue(self, undefined)
}
self._value = newValue
self._dirty = false
self._value = self.effect.run()!
self._scheduled = false
}
self._deferredComputeds.length = 0
return self._value
}

Expand Down
6 changes: 3 additions & 3 deletions packages/reactivity/src/deferredComputed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class DeferredComputedRefImpl<T> {
let compareTarget: any
let hasCompareTarget = false
let scheduled = false
this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
this.effect = new ReactiveEffect(getter, (_deferredComputed, computedTrigger?: boolean) => {
if (this.dep) {
if (computedTrigger) {
compareTarget = this._value
Expand All @@ -49,7 +49,7 @@ class DeferredComputedRefImpl<T> {
hasCompareTarget = false
scheduler(() => {
if (this.effect.active && this._get() !== valueToCompare) {
triggerRefValue(this)
triggerRefValue(this, undefined)
}
scheduled = false
})
Expand All @@ -59,7 +59,7 @@ class DeferredComputedRefImpl<T> {
// deferred to be triggered in scheduler.
for (const e of this.dep) {
if (e.computed instanceof DeferredComputedRefImpl) {
e.scheduler!(true /* computedTrigger */)
e.scheduler!(undefined, true /* computedTrigger */)
}
}
}
Expand Down
69 changes: 60 additions & 9 deletions packages/reactivity/src/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export let trackOpBit = 1
*/
const maxMarkerBits = 30

export type EffectScheduler = (...args: any[]) => any
export type EffectScheduler = (
deferredComputed: ComputedRefImpl<any> | undefined,
...args: any[]
) => any

export type DebuggerEvent = {
effect: ReactiveEffect
Expand Down Expand Up @@ -185,7 +188,23 @@ export function effect<T = any>(
fn = (fn as ReactiveEffectRunner).effect.fn
}

const _effect = new ReactiveEffect(fn)
let _dirty = false
let _scheduled = false
let _deferredComputeds: ComputedRefImpl<any>[] = []

const _effect = new ReactiveEffect(fn, deferredComputed => {
if (!_dirty) {
if (deferredComputed) {
_deferredComputeds.push(deferredComputed)
} else {
_dirty = true
}
if (!_scheduled) {
_scheduled = true
pendingEffectRunners.push(run)
}
}
})
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
Expand All @@ -196,6 +215,28 @@ export function effect<T = any>(
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner

function run() {
if (!_dirty && _deferredComputeds.length) {
if (_deferredComputeds.length >= 2) {
_deferredComputeds = _deferredComputeds.sort(
(a, b) => _effect.deps.indexOf(a.dep!) - _effect.deps.indexOf(b.dep!)
)
}
for (const deferredComputed of _deferredComputeds) {
deferredComputed.value
if (_dirty) {
break
}
}
}
if (_dirty) {
_dirty = false
_effect.run()
}
_deferredComputeds.length = 0
_scheduled = false
}
}

/**
Expand Down Expand Up @@ -370,9 +411,9 @@ export function trigger(
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
triggerEffects(deps[0], undefined, eventInfo)
} else {
triggerEffects(deps[0])
triggerEffects(deps[0], undefined)
}
}
} else {
Expand All @@ -383,45 +424,55 @@ export function trigger(
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
triggerEffects(createDep(effects), undefined, eventInfo)
} else {
triggerEffects(createDep(effects))
triggerEffects(createDep(effects), undefined)
}
}
}

export function triggerEffects(
dep: Dep | ReactiveEffect[],
deferredComputed: ComputedRefImpl<any> | undefined,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
triggerEffect(effect, deferredComputed, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
triggerEffect(effect, deferredComputed, debuggerEventExtraInfo)
}
}
}

const pendingEffectRunners: (() => void)[] = []

function triggerEffect(
effect: ReactiveEffect,
deferredComputed: ComputedRefImpl<any> | undefined,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
const isRootTrigger = effectTrackDepth === 0
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
effect.scheduler(deferredComputed)
} else {
effect.run()
}
}
if (isRootTrigger) {
while (pendingEffectRunners.length) {
pendingEffectRunners.shift()!()
}
}
}

export function getDepFromReactive(object: any, key: string | number | symbol) {
Expand Down
17 changes: 11 additions & 6 deletions packages/reactivity/src/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
isShallow
} from './reactive'
import type { ShallowReactiveMarker } from './reactive'
import type { ComputedRefImpl } from './computed'
import { CollectionTypes } from './collectionHandlers'
import { createDep, Dep } from './dep'

Expand Down Expand Up @@ -52,19 +53,23 @@ export function trackRefValue(ref: RefBase<any>) {
}
}

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
export function triggerRefValue(
ref: RefBase<any>,
deferredComputed: ComputedRefImpl<any> | undefined,
newVal?: any
) {
ref = toRaw(ref)
const dep = ref.dep
if (dep) {
if (__DEV__) {
triggerEffects(dep, {
triggerEffects(dep, deferredComputed, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(dep)
triggerEffects(dep, deferredComputed)
}
}
}
Expand Down Expand Up @@ -155,7 +160,7 @@ class RefImpl<T> {
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
triggerRefValue(this, undefined, newVal)
}
}
}
Expand Down Expand Up @@ -186,7 +191,7 @@ class RefImpl<T> {
* @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref}
*/
export function triggerRef(ref: Ref) {
triggerRefValue(ref, __DEV__ ? ref.value : void 0)
triggerRefValue(ref, undefined, __DEV__ ? ref.value : void 0)
}

export type MaybeRef<T = any> = T | Ref<T>
Expand Down Expand Up @@ -282,7 +287,7 @@ class CustomRefImpl<T> {
constructor(factory: CustomRefFactory<T>) {
const { get, set } = factory(
() => trackRefValue(this),
() => triggerRefValue(this)
() => triggerRefValue(this, undefined)
)
this._get = get
this._set = set
Expand Down