refactor: sync value access for chained computed w/ scheduler
This commit is contained in:
parent
02339b67d8
commit
1fe2239270
@ -201,10 +201,29 @@ describe('reactivity/computed', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('with scheduler', () => {
|
describe('with scheduler', () => {
|
||||||
const p = Promise.resolve()
|
// a simple scheduler similar to the main Vue scheduler
|
||||||
const defer = (fn?: any) => (fn ? p.then(fn) : p)
|
const tick = Promise.resolve()
|
||||||
|
const queue: any[] = []
|
||||||
|
let queued = false
|
||||||
|
|
||||||
|
const schedule = (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
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setComputedScheduler(defer)
|
setComputedScheduler(schedule)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -224,7 +243,7 @@ describe('reactivity/computed', () => {
|
|||||||
src.value = 3
|
src.value = 3
|
||||||
// not called yet
|
// not called yet
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
await defer()
|
await tick
|
||||||
// should only trigger once
|
// should only trigger once
|
||||||
expect(spy).toHaveBeenCalledTimes(2)
|
expect(spy).toHaveBeenCalledTimes(2)
|
||||||
expect(spy).toHaveBeenCalledWith(c.value)
|
expect(spy).toHaveBeenCalledWith(c.value)
|
||||||
@ -241,16 +260,67 @@ describe('reactivity/computed', () => {
|
|||||||
src.value = 1
|
src.value = 1
|
||||||
src.value = 2
|
src.value = 2
|
||||||
|
|
||||||
await defer()
|
await tick
|
||||||
// should not trigger
|
// should not trigger
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
src.value = 3
|
src.value = 3
|
||||||
src.value = 4
|
src.value = 4
|
||||||
src.value = 5
|
src.value = 5
|
||||||
await defer()
|
await tick
|
||||||
// should trigger because latest value changes
|
// should trigger because latest value changes
|
||||||
expect(spy).toHaveBeenCalledTimes(2)
|
expect(spy).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('chained computed value invalidation', async () => {
|
||||||
|
const effectSpy = jest.fn()
|
||||||
|
const c1Spy = jest.fn()
|
||||||
|
const c2Spy = jest.fn()
|
||||||
|
|
||||||
|
const src = ref(0)
|
||||||
|
const c1 = computed(() => {
|
||||||
|
c1Spy()
|
||||||
|
return src.value % 2
|
||||||
|
})
|
||||||
|
const c2 = computed(() => {
|
||||||
|
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)
|
||||||
|
await tick
|
||||||
|
expect(effectSpy).toHaveBeenCalledTimes(2)
|
||||||
|
expect(effectSpy).toHaveBeenCalledWith(2)
|
||||||
|
expect(c1Spy).toHaveBeenCalledTimes(2)
|
||||||
|
expect(c2Spy).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
|
src.value = 2
|
||||||
|
await tick
|
||||||
|
expect(effectSpy).toHaveBeenCalledTimes(3)
|
||||||
|
expect(c1Spy).toHaveBeenCalledTimes(3)
|
||||||
|
expect(c2Spy).toHaveBeenCalledTimes(3)
|
||||||
|
|
||||||
|
src.value = 4
|
||||||
|
await tick
|
||||||
|
expect(effectSpy).toHaveBeenCalledTimes(3)
|
||||||
|
expect(c1Spy).toHaveBeenCalledTimes(4)
|
||||||
|
// in-between chained computed always re-compute, but it does avoid
|
||||||
|
// triggering the final subscribing effect.
|
||||||
|
expect(c2Spy).toHaveBeenCalledTimes(4)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { ReactiveEffect } from './effect'
|
import { ReactiveEffect, triggerEffects } from './effect'
|
||||||
import { Ref, trackRefValue, triggerRefValue } from './ref'
|
import { Ref, trackRefValue, triggerRefValue } from './ref'
|
||||||
import { isFunction, NOOP } from '@vue/shared'
|
import { isFunction, NOOP } from '@vue/shared'
|
||||||
import { ReactiveFlags, toRaw } from './reactive'
|
import { ReactiveFlags, toRaw } from './reactive'
|
||||||
import { Dep } from './dep'
|
import { Dep } from './dep'
|
||||||
|
import { TriggerOpTypes } from '@vue/runtime-core'
|
||||||
|
|
||||||
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
|
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
|
||||||
readonly value: T
|
readonly value: T
|
||||||
@ -35,6 +36,7 @@ class ComputedRefImpl<T> {
|
|||||||
|
|
||||||
private _value!: T
|
private _value!: T
|
||||||
private _dirty = true
|
private _dirty = true
|
||||||
|
private _changed = false
|
||||||
|
|
||||||
public readonly effect: ReactiveEffect<T>
|
public readonly effect: ReactiveEffect<T>
|
||||||
|
|
||||||
@ -46,46 +48,58 @@ class ComputedRefImpl<T> {
|
|||||||
private readonly _setter: ComputedSetter<T>,
|
private readonly _setter: ComputedSetter<T>,
|
||||||
isReadonly: boolean
|
isReadonly: boolean
|
||||||
) {
|
) {
|
||||||
let deferFn: () => void
|
|
||||||
let scheduled = false
|
|
||||||
this.effect = new ReactiveEffect(getter, () => {
|
this.effect = new ReactiveEffect(getter, () => {
|
||||||
if (!this._dirty) {
|
if (!this._dirty) {
|
||||||
this._dirty = true
|
this._dirty = true
|
||||||
if (scheduler) {
|
if (scheduler) {
|
||||||
if (!scheduled) {
|
if (this.dep) {
|
||||||
scheduled = true
|
const effects: ReactiveEffect[] = []
|
||||||
scheduler(
|
scheduler(() => {
|
||||||
deferFn ||
|
if ((this._get(), this._changed)) {
|
||||||
(deferFn = () => {
|
if (__DEV__) {
|
||||||
scheduled = false
|
triggerEffects(effects, {
|
||||||
if (this._dirty) {
|
target: this,
|
||||||
this._dirty = false
|
type: TriggerOpTypes.SET,
|
||||||
const newValue = this.effect.run()!
|
key: 'value',
|
||||||
if (this._value !== newValue) {
|
newValue: this._value
|
||||||
this._value = newValue
|
})
|
||||||
triggerRefValue(this)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
triggerRefValue(this)
|
triggerEffects(effects)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
// 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!()
|
||||||
|
} else {
|
||||||
|
effects.push(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
triggerRefValue(this)
|
triggerRefValue(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this.effect.computed = true
|
||||||
this[ReactiveFlags.IS_READONLY] = isReadonly
|
this[ReactiveFlags.IS_READONLY] = isReadonly
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _get() {
|
||||||
|
if (this._dirty) {
|
||||||
|
const oldValue = this._value
|
||||||
|
this._changed = oldValue !== (this._value = this.effect.run()!)
|
||||||
|
this._dirty = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
|
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
|
||||||
const self = toRaw(this)
|
const self = toRaw(this)
|
||||||
if (self._dirty) {
|
self._get()
|
||||||
self._value = self.effect.run()!
|
|
||||||
self._dirty = false
|
|
||||||
}
|
|
||||||
trackRefValue(self)
|
trackRefValue(self)
|
||||||
return self._value
|
return self._value
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,8 @@ export class ReactiveEffect<T = any> {
|
|||||||
deps: Dep[] = []
|
deps: Dep[] = []
|
||||||
|
|
||||||
// can be attached after creation
|
// can be attached after creation
|
||||||
|
computed?: boolean
|
||||||
|
allowRecurse?: boolean
|
||||||
onStop?: () => void
|
onStop?: () => void
|
||||||
// dev only
|
// dev only
|
||||||
onTrack?: (event: DebuggerEvent) => void
|
onTrack?: (event: DebuggerEvent) => void
|
||||||
@ -64,9 +66,7 @@ export class ReactiveEffect<T = any> {
|
|||||||
constructor(
|
constructor(
|
||||||
public fn: () => T,
|
public fn: () => T,
|
||||||
public scheduler: EffectScheduler | null = null,
|
public scheduler: EffectScheduler | null = null,
|
||||||
scope?: EffectScope | null,
|
scope?: EffectScope | null
|
||||||
// allow recursive self-invocation
|
|
||||||
public allowRecurse = false
|
|
||||||
) {
|
) {
|
||||||
recordEffectScope(this, scope)
|
recordEffectScope(this, scope)
|
||||||
}
|
}
|
||||||
@ -303,7 +303,11 @@ export function trigger(
|
|||||||
|
|
||||||
if (deps.length === 1) {
|
if (deps.length === 1) {
|
||||||
if (deps[0]) {
|
if (deps[0]) {
|
||||||
|
if (__DEV__) {
|
||||||
triggerEffects(deps[0], eventInfo)
|
triggerEffects(deps[0], eventInfo)
|
||||||
|
} else {
|
||||||
|
triggerEffects(deps[0])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const effects: ReactiveEffect[] = []
|
const effects: ReactiveEffect[] = []
|
||||||
@ -312,16 +316,20 @@ export function trigger(
|
|||||||
effects.push(...dep)
|
effects.push(...dep)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (__DEV__) {
|
||||||
triggerEffects(createDep(effects), eventInfo)
|
triggerEffects(createDep(effects), eventInfo)
|
||||||
|
} else {
|
||||||
|
triggerEffects(createDep(effects))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function triggerEffects(
|
export function triggerEffects(
|
||||||
dep: Dep,
|
dep: Dep | ReactiveEffect[],
|
||||||
debuggerEventExtraInfo?: DebuggerEventExtraInfo
|
debuggerEventExtraInfo?: DebuggerEventExtraInfo
|
||||||
) {
|
) {
|
||||||
// spread into array for stabilization
|
// spread into array for stabilization
|
||||||
for (const effect of [...dep]) {
|
for (const effect of isArray(dep) ? dep : [...dep]) {
|
||||||
if (effect !== activeEffect || effect.allowRecurse) {
|
if (effect !== activeEffect || effect.allowRecurse) {
|
||||||
if (__DEV__ && effect.onTrigger) {
|
if (__DEV__ && effect.onTrigger) {
|
||||||
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
|
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
|
||||||
|
@ -1630,15 +1630,14 @@ function baseCreateRenderer(
|
|||||||
const effect = new ReactiveEffect(
|
const effect = new ReactiveEffect(
|
||||||
componentUpdateFn,
|
componentUpdateFn,
|
||||||
() => queueJob(instance.update),
|
() => queueJob(instance.update),
|
||||||
instance.scope, // track it in component's effect scope
|
instance.scope // track it in component's effect scope
|
||||||
true /* allowRecurse */
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
|
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
|
||||||
update.id = instance.uid
|
update.id = instance.uid
|
||||||
// allowRecurse
|
// allowRecurse
|
||||||
// #1801, #2043 component render effects should allow recursive updates
|
// #1801, #2043 component render effects should allow recursive updates
|
||||||
update.allowRecurse = true
|
effect.allowRecurse = update.allowRecurse = true
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
effect.onTrack = instance.rtc
|
effect.onTrack = instance.rtc
|
||||||
|
Loading…
Reference in New Issue
Block a user