-
-
Notifications
You must be signed in to change notification settings - Fork 8.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Note: this is not exposed as part of Vue API, only as a lower-level API specific to @vue/reactivity
- Loading branch information
Showing
3 changed files
with
274 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
import { computed, deferredComputed, effect, ref } from '../src' | ||
|
||
describe('deferred computed', () => { | ||
const tick = Promise.resolve() | ||
|
||
test('should only trigger once on multiple mutations', async () => { | ||
const src = ref(0) | ||
const c = deferredComputed(() => src.value) | ||
const spy = jest.fn() | ||
effect(() => { | ||
spy(c.value) | ||
}) | ||
expect(spy).toHaveBeenCalledTimes(1) | ||
src.value = 1 | ||
src.value = 2 | ||
src.value = 3 | ||
// not called yet | ||
expect(spy).toHaveBeenCalledTimes(1) | ||
await tick | ||
// should only trigger once | ||
expect(spy).toHaveBeenCalledTimes(2) | ||
expect(spy).toHaveBeenCalledWith(c.value) | ||
}) | ||
|
||
test('should not trigger if value did not change', async () => { | ||
const src = ref(0) | ||
const c = deferredComputed(() => src.value % 2) | ||
const spy = jest.fn() | ||
effect(() => { | ||
spy(c.value) | ||
}) | ||
expect(spy).toHaveBeenCalledTimes(1) | ||
src.value = 1 | ||
src.value = 2 | ||
|
||
await tick | ||
// should not trigger | ||
expect(spy).toHaveBeenCalledTimes(1) | ||
|
||
src.value = 3 | ||
src.value = 4 | ||
src.value = 5 | ||
await tick | ||
// should trigger because latest value changes | ||
expect(spy).toHaveBeenCalledTimes(2) | ||
}) | ||
|
||
test('chained computed trigger', async () => { | ||
const effectSpy = jest.fn() | ||
const c1Spy = jest.fn() | ||
const c2Spy = jest.fn() | ||
|
||
const src = ref(0) | ||
const c1 = deferredComputed(() => { | ||
c1Spy() | ||
return src.value % 2 | ||
}) | ||
const c2 = computed(() => { | ||
c2Spy() | ||
return c1.value + 1 | ||
}) | ||
|
||
effect(() => { | ||
effectSpy(c2.value) | ||
}) | ||
|
||
expect(c1Spy).toHaveBeenCalledTimes(1) | ||
expect(c2Spy).toHaveBeenCalledTimes(1) | ||
expect(effectSpy).toHaveBeenCalledTimes(1) | ||
|
||
src.value = 1 | ||
await tick | ||
expect(c1Spy).toHaveBeenCalledTimes(2) | ||
expect(c2Spy).toHaveBeenCalledTimes(2) | ||
expect(effectSpy).toHaveBeenCalledTimes(2) | ||
}) | ||
|
||
test('chained computed avoid re-compute', async () => { | ||
const effectSpy = jest.fn() | ||
const c1Spy = jest.fn() | ||
const c2Spy = jest.fn() | ||
|
||
const src = ref(0) | ||
const c1 = deferredComputed(() => { | ||
c1Spy() | ||
return src.value % 2 | ||
}) | ||
const c2 = computed(() => { | ||
c2Spy() | ||
return c1.value + 1 | ||
}) | ||
|
||
effect(() => { | ||
effectSpy(c2.value) | ||
}) | ||
|
||
expect(effectSpy).toHaveBeenCalledTimes(1) | ||
src.value = 2 | ||
src.value = 4 | ||
src.value = 6 | ||
await tick | ||
// c1 should re-compute once. | ||
expect(c1Spy).toHaveBeenCalledTimes(2) | ||
// c2 should not have to re-compute because c1 did not change. | ||
expect(c2Spy).toHaveBeenCalledTimes(1) | ||
// effect should not trigger because c2 did not change. | ||
expect(effectSpy).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
test('chained computed value invalidation', async () => { | ||
const effectSpy = jest.fn() | ||
const c1Spy = jest.fn() | ||
const c2Spy = jest.fn() | ||
|
||
const src = ref(0) | ||
const c1 = deferredComputed(() => { | ||
c1Spy() | ||
return src.value % 2 | ||
}) | ||
const c2 = deferredComputed(() => { | ||
c2Spy() | ||
return c1.value + 1 | ||
}) | ||
|
||
effect(() => { | ||
effectSpy(c2.value) | ||
}) | ||
|
||
expect(effectSpy).toHaveBeenCalledTimes(1) | ||
expect(effectSpy).toHaveBeenCalledWith(1) | ||
expect(c2.value).toBe(1) | ||
|
||
expect(c1Spy).toHaveBeenCalledTimes(1) | ||
expect(c2Spy).toHaveBeenCalledTimes(1) | ||
|
||
src.value = 1 | ||
// value should be available sync | ||
expect(c2.value).toBe(2) | ||
expect(c2Spy).toHaveBeenCalledTimes(2) | ||
}) | ||
|
||
test('sync access of invalidated chained computed should not prevent final effect from running', async () => { | ||
const effectSpy = jest.fn() | ||
const c1Spy = jest.fn() | ||
const c2Spy = jest.fn() | ||
|
||
const src = ref(0) | ||
const c1 = deferredComputed(() => { | ||
c1Spy() | ||
return src.value % 2 | ||
}) | ||
const c2 = deferredComputed(() => { | ||
c2Spy() | ||
return c1.value + 1 | ||
}) | ||
|
||
effect(() => { | ||
effectSpy(c2.value) | ||
}) | ||
expect(effectSpy).toHaveBeenCalledTimes(1) | ||
|
||
src.value = 1 | ||
// sync access c2 | ||
c2.value | ||
await tick | ||
expect(effectSpy).toHaveBeenCalledTimes(2) | ||
}) | ||
|
||
test('should not compute if deactivated before scheduler is called', async () => { | ||
const c1Spy = jest.fn() | ||
const src = ref(0) | ||
const c1 = deferredComputed(() => { | ||
c1Spy() | ||
return src.value % 2 | ||
}) | ||
effect(() => c1.value) | ||
expect(c1Spy).toHaveBeenCalledTimes(1) | ||
|
||
c1.effect.stop() | ||
// trigger | ||
src.value++ | ||
await tick | ||
expect(c1Spy).toHaveBeenCalledTimes(1) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { Dep } from './dep' | ||
import { ReactiveEffect } from './effect' | ||
import { ComputedGetter, ComputedRef } from './computed' | ||
import { ReactiveFlags, toRaw } from './reactive' | ||
import { trackRefValue, triggerRefValue } from './ref' | ||
|
||
const tick = Promise.resolve() | ||
const queue: any[] = [] | ||
let queued = false | ||
|
||
const scheduler = (fn: any) => { | ||
queue.push(fn) | ||
if (!queued) { | ||
queued = true | ||
tick.then(flush) | ||
} | ||
} | ||
|
||
const flush = () => { | ||
for (let i = 0; i < queue.length; i++) { | ||
queue[i]() | ||
} | ||
queue.length = 0 | ||
queued = false | ||
} | ||
|
||
class DeferredComputedRefImpl<T> { | ||
public dep?: Dep = undefined | ||
|
||
private _value!: T | ||
private _dirty = true | ||
public readonly effect: ReactiveEffect<T> | ||
|
||
public readonly __v_isRef = true | ||
public readonly [ReactiveFlags.IS_READONLY] = true | ||
|
||
constructor(getter: ComputedGetter<T>) { | ||
let compareTarget: any | ||
let hasCompareTarget = false | ||
let scheduled = false | ||
this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => { | ||
if (this.dep) { | ||
if (computedTrigger) { | ||
compareTarget = this._value | ||
hasCompareTarget = true | ||
} else if (!scheduled) { | ||
const valueToCompare = hasCompareTarget ? compareTarget : this._value | ||
scheduled = true | ||
hasCompareTarget = false | ||
scheduler(() => { | ||
if (this.effect.active && this._get() !== valueToCompare) { | ||
triggerRefValue(this) | ||
} | ||
scheduled = false | ||
}) | ||
} | ||
// chained upstream computeds are notified synchronously to ensure | ||
// value invalidation in case of sync access; normal effects are | ||
// deferred to be triggered in scheduler. | ||
for (const e of this.dep) { | ||
if (e.computed) { | ||
e.scheduler!(true /* computedTrigger */) | ||
} | ||
} | ||
} | ||
this._dirty = true | ||
}) | ||
this.effect.computed = true | ||
} | ||
|
||
private _get() { | ||
if (this._dirty) { | ||
this._dirty = false | ||
return (this._value = this.effect.run()!) | ||
} | ||
return this._value | ||
} | ||
|
||
get value() { | ||
trackRefValue(this) | ||
// the computed ref may get wrapped by other proxies e.g. readonly() #3376 | ||
return toRaw(this)._get() | ||
} | ||
} | ||
|
||
export function deferredComputed<T>(getter: () => T): ComputedRef<T> { | ||
return new DeferredComputedRefImpl(getter) as any | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters