Skip to content

Commit 2dd4739

Browse files
committed
test: more test cases for computed w/ scheduler
1 parent 1fe2239 commit 2dd4739

File tree

3 files changed

+120
-58
lines changed

3 files changed

+120
-58
lines changed

packages/reactivity/__tests__/computed.spec.ts

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,68 @@ describe('reactivity/computed', () => {
272272
expect(spy).toHaveBeenCalledTimes(2)
273273
})
274274

275+
test('chained computed trigger', async () => {
276+
const effectSpy = jest.fn()
277+
const c1Spy = jest.fn()
278+
const c2Spy = jest.fn()
279+
280+
const src = ref(0)
281+
const c1 = computed(() => {
282+
c1Spy()
283+
return src.value % 2
284+
})
285+
const c2 = computed(() => {
286+
c2Spy()
287+
return c1.value + 1
288+
})
289+
290+
effect(() => {
291+
effectSpy(c2.value)
292+
})
293+
294+
expect(c1Spy).toHaveBeenCalledTimes(1)
295+
expect(c2Spy).toHaveBeenCalledTimes(1)
296+
expect(effectSpy).toHaveBeenCalledTimes(1)
297+
298+
src.value = 1
299+
await tick
300+
expect(c1Spy).toHaveBeenCalledTimes(2)
301+
expect(c2Spy).toHaveBeenCalledTimes(2)
302+
expect(effectSpy).toHaveBeenCalledTimes(2)
303+
})
304+
305+
test('chained computed avoid re-compute', async () => {
306+
const effectSpy = jest.fn()
307+
const c1Spy = jest.fn()
308+
const c2Spy = jest.fn()
309+
310+
const src = ref(0)
311+
const c1 = computed(() => {
312+
c1Spy()
313+
return src.value % 2
314+
})
315+
const c2 = computed(() => {
316+
c2Spy()
317+
return c1.value + 1
318+
})
319+
320+
effect(() => {
321+
effectSpy(c2.value)
322+
})
323+
324+
expect(effectSpy).toHaveBeenCalledTimes(1)
325+
src.value = 2
326+
src.value = 4
327+
src.value = 6
328+
await tick
329+
// c1 should re-compute once.
330+
expect(c1Spy).toHaveBeenCalledTimes(2)
331+
// c2 should not have to re-compute because c1 did not change.
332+
expect(c2Spy).toHaveBeenCalledTimes(1)
333+
// effect should not trigger because c2 did not change.
334+
expect(effectSpy).toHaveBeenCalledTimes(1)
335+
})
336+
275337
test('chained computed value invalidation', async () => {
276338
const effectSpy = jest.fn()
277339
const c1Spy = jest.fn()
@@ -302,25 +364,33 @@ describe('reactivity/computed', () => {
302364
// value should be available sync
303365
expect(c2.value).toBe(2)
304366
expect(c2Spy).toHaveBeenCalledTimes(2)
305-
await tick
306-
expect(effectSpy).toHaveBeenCalledTimes(2)
307-
expect(effectSpy).toHaveBeenCalledWith(2)
308-
expect(c1Spy).toHaveBeenCalledTimes(2)
309-
expect(c2Spy).toHaveBeenCalledTimes(2)
367+
})
310368

311-
src.value = 2
312-
await tick
313-
expect(effectSpy).toHaveBeenCalledTimes(3)
314-
expect(c1Spy).toHaveBeenCalledTimes(3)
315-
expect(c2Spy).toHaveBeenCalledTimes(3)
369+
test('sync access of invalidated chained computed should not prevent final effect from running', async () => {
370+
const effectSpy = jest.fn()
371+
const c1Spy = jest.fn()
372+
const c2Spy = jest.fn()
316373

317-
src.value = 4
374+
const src = ref(0)
375+
const c1 = computed(() => {
376+
c1Spy()
377+
return src.value % 2
378+
})
379+
const c2 = computed(() => {
380+
c2Spy()
381+
return c1.value + 1
382+
})
383+
384+
effect(() => {
385+
effectSpy(c2.value)
386+
})
387+
expect(effectSpy).toHaveBeenCalledTimes(1)
388+
389+
src.value = 1
390+
// sync access c2
391+
c2.value
318392
await tick
319-
expect(effectSpy).toHaveBeenCalledTimes(3)
320-
expect(c1Spy).toHaveBeenCalledTimes(4)
321-
// in-between chained computed always re-compute, but it does avoid
322-
// triggering the final subscribing effect.
323-
expect(c2Spy).toHaveBeenCalledTimes(4)
393+
expect(effectSpy).toHaveBeenCalledTimes(2)
324394
})
325395
})
326396
})

packages/reactivity/src/computed.ts

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { ReactiveEffect, triggerEffects } from './effect'
1+
import { ReactiveEffect } from './effect'
22
import { Ref, trackRefValue, triggerRefValue } from './ref'
33
import { isFunction, NOOP } from '@vue/shared'
44
import { ReactiveFlags, toRaw } from './reactive'
55
import { Dep } from './dep'
6-
import { TriggerOpTypes } from '@vue/runtime-core'
76

87
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
98
readonly value: T
@@ -36,8 +35,6 @@ class ComputedRefImpl<T> {
3635

3736
private _value!: T
3837
private _dirty = true
39-
private _changed = false
40-
4138
public readonly effect: ReactiveEffect<T>
4239

4340
public readonly __v_isRef = true;
@@ -48,60 +45,55 @@ class ComputedRefImpl<T> {
4845
private readonly _setter: ComputedSetter<T>,
4946
isReadonly: boolean
5047
) {
51-
this.effect = new ReactiveEffect(getter, () => {
52-
if (!this._dirty) {
53-
this._dirty = true
54-
if (scheduler) {
55-
if (this.dep) {
56-
const effects: ReactiveEffect[] = []
57-
scheduler(() => {
58-
if ((this._get(), this._changed)) {
59-
if (__DEV__) {
60-
triggerEffects(effects, {
61-
target: this,
62-
type: TriggerOpTypes.SET,
63-
key: 'value',
64-
newValue: this._value
65-
})
66-
} else {
67-
triggerEffects(effects)
68-
}
69-
}
70-
})
71-
// chained upstream computeds are notified synchronously to ensure
72-
// value invalidation in case of sync access; normal effects are
73-
// deferred to be triggered in scheduler.
74-
for (const e of this.dep) {
75-
if (e.computed) {
76-
e.scheduler!()
77-
} else {
78-
effects.push(e)
79-
}
48+
let compareTarget: any
49+
let hasCompareTarget = false
50+
let scheduled = false
51+
this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
52+
if (scheduler && this.dep) {
53+
if (computedTrigger) {
54+
compareTarget = this._value
55+
hasCompareTarget = true
56+
} else if (!scheduled) {
57+
const valueToCompare = hasCompareTarget ? compareTarget : this._value
58+
scheduled = true
59+
hasCompareTarget = false
60+
scheduler(() => {
61+
if (this._get() !== valueToCompare) {
62+
triggerRefValue(this)
8063
}
64+
scheduled = false
65+
})
66+
}
67+
// chained upstream computeds are notified synchronously to ensure
68+
// value invalidation in case of sync access; normal effects are
69+
// deferred to be triggered in scheduler.
70+
for (const e of this.dep) {
71+
if (e.computed) {
72+
e.scheduler!(true /* computedTrigger */)
8173
}
82-
} else {
83-
triggerRefValue(this)
8474
}
8575
}
76+
if (!this._dirty) {
77+
this._dirty = true
78+
if (!scheduler) triggerRefValue(this)
79+
}
8680
})
8781
this.effect.computed = true
8882
this[ReactiveFlags.IS_READONLY] = isReadonly
8983
}
9084

9185
private _get() {
9286
if (this._dirty) {
93-
const oldValue = this._value
94-
this._changed = oldValue !== (this._value = this.effect.run()!)
9587
this._dirty = false
88+
return (this._value = this.effect.run()!)
9689
}
90+
return this._value
9791
}
9892

9993
get value() {
94+
trackRefValue(this)
10095
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
101-
const self = toRaw(this)
102-
self._get()
103-
trackRefValue(self)
104-
return self._value
96+
return toRaw(this)._get()
10597
}
10698

10799
set value(newValue: T) {

packages/reactivity/src/effect.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export let trackOpBit = 1
2929
*/
3030
const maxMarkerBits = 30
3131

32-
export type EffectScheduler = () => void
32+
export type EffectScheduler = (...args: any[]) => any
3333

3434
export type DebuggerEvent = {
3535
effect: ReactiveEffect

0 commit comments

Comments
 (0)