Skip to content

Commit abc27e4

Browse files
committed
perf: calculate embedded computed() on-demand
1 parent 2a9e9a4 commit abc27e4

File tree

9 files changed

+99
-34
lines changed

9 files changed

+99
-34
lines changed

packages/reactivity/__tests__/computed.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,55 @@ describe('reactivity/computed', () => {
289289
oldValue: 2
290290
})
291291
})
292+
293+
it('chained computed value on-demand trigger', () => {
294+
const c1Spy = jest.fn()
295+
const c2Spy = jest.fn()
296+
297+
const src = ref(0)
298+
const c1 = computed(() => {
299+
c1Spy()
300+
return src.value < 5
301+
})
302+
const c2 = computed(() => {
303+
c2Spy()
304+
return c1.value ? '< 5' : '>= 5'
305+
})
306+
307+
expect(c1Spy).toHaveBeenCalledTimes(0)
308+
expect(c2Spy).toHaveBeenCalledTimes(0)
309+
310+
expect(src.value).toBe(0)
311+
expect(c2.value).toBe('< 5')
312+
expect(c1Spy).toHaveBeenCalledTimes(1)
313+
expect(c2Spy).toHaveBeenCalledTimes(1)
314+
315+
src.value++
316+
expect(c2.value).toBe('< 5')
317+
expect(c1Spy).toHaveBeenCalledTimes(2)
318+
expect(c2Spy).toHaveBeenCalledTimes(1)
319+
320+
for (let i = 0; i < 10; i++) {
321+
src.value++
322+
}
323+
expect(src.value).toBe(11)
324+
expect(c2.value).toBe('>= 5')
325+
expect(c1Spy).toHaveBeenCalledTimes(3)
326+
expect(c2Spy).toHaveBeenCalledTimes(2)
327+
328+
src.value++
329+
expect(src.value).toBe(12)
330+
expect(c2.value).toBe('>= 5')
331+
expect(c1Spy).toHaveBeenCalledTimes(4)
332+
expect(c2Spy).toHaveBeenCalledTimes(2)
333+
334+
for (let i = 0; i < 100; i++) {
335+
src.value++
336+
c2.value
337+
}
338+
expect(src.value).toBe(112)
339+
expect(c2.value).toBe('>= 5')
340+
expect(c1Spy).toHaveBeenCalledTimes(104)
341+
expect(c2Spy).toHaveBeenCalledTimes(2)
342+
})
292343
})

packages/reactivity/src/baseHandlers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,9 @@ function createSetter(shallow = false) {
179179
// don't trigger if target is something up in the prototype chain of original
180180
if (target === toRaw(receiver)) {
181181
if (!hadKey) {
182-
trigger(target, TriggerOpTypes.ADD, key, value)
182+
trigger(target, TriggerOpTypes.ADD, undefined, key, value)
183183
} else if (hasChanged(value, oldValue)) {
184-
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
184+
trigger(target, TriggerOpTypes.SET, undefined, key, value, oldValue)
185185
}
186186
}
187187
return result

packages/reactivity/src/collectionHandlers.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ function add(this: SetTypes, value: unknown) {
7373
const hadKey = proto.has.call(target, value)
7474
if (!hadKey) {
7575
target.add(value)
76-
trigger(target, TriggerOpTypes.ADD, value, value)
76+
trigger(target, TriggerOpTypes.ADD, undefined, value, value)
7777
}
7878
return this
7979
}
@@ -94,9 +94,9 @@ function set(this: MapTypes, key: unknown, value: unknown) {
9494
const oldValue = get.call(target, key)
9595
target.set(key, value)
9696
if (!hadKey) {
97-
trigger(target, TriggerOpTypes.ADD, key, value)
97+
trigger(target, TriggerOpTypes.ADD, undefined, key, value)
9898
} else if (hasChanged(value, oldValue)) {
99-
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
99+
trigger(target, TriggerOpTypes.SET, undefined, key, value, oldValue)
100100
}
101101
return this
102102
}
@@ -116,7 +116,7 @@ function deleteEntry(this: CollectionTypes, key: unknown) {
116116
// forward the operation before queueing reactions
117117
const result = target.delete(key)
118118
if (hadKey) {
119-
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
119+
trigger(target, TriggerOpTypes.DELETE, undefined, key, undefined, oldValue)
120120
}
121121
return result
122122
}

packages/reactivity/src/computed.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export class ComputedRefImpl<T> {
3333
public readonly [ReactiveFlags.IS_READONLY]: boolean
3434

3535
public _dirty = true
36+
public _computedsToAskDirty: ComputedRefImpl<any>[] = []
3637
public _cacheable: boolean
3738

3839
constructor(
@@ -41,10 +42,13 @@ export class ComputedRefImpl<T> {
4142
isReadonly: boolean,
4243
isSSR: boolean
4344
) {
44-
this.effect = new ReactiveEffect(getter, () => {
45-
if (!this._dirty) {
45+
this.effect = new ReactiveEffect(getter, (_c) => {
46+
if (_c) {
47+
this._computedsToAskDirty.push(_c)
48+
}
49+
else if (!this._dirty) {
4650
this._dirty = true
47-
triggerRefValue(this)
51+
triggerRefValue(this, this)
4852
}
4953
})
5054
this.effect.computed = this
@@ -55,11 +59,24 @@ export class ComputedRefImpl<T> {
5559
get value() {
5660
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
5761
const self = toRaw(this)
62+
if (!self._dirty) {
63+
for (const computedToAskDirty of self._computedsToAskDirty) {
64+
computedToAskDirty.value
65+
if (self._dirty) {
66+
break
67+
}
68+
}
69+
}
5870
trackRefValue(self)
5971
if (self._dirty || !self._cacheable) {
72+
const newValue = self.effect.run()!
73+
if (self._value !== newValue) {
74+
triggerRefValue(this, undefined)
75+
}
76+
self._value = newValue
6077
self._dirty = false
61-
self._value = self.effect.run()!
6278
}
79+
self._computedsToAskDirty.length = 0
6380
return self._value
6481
}
6582

packages/reactivity/src/deferredComputed.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class DeferredComputedRefImpl<T> {
3838
let compareTarget: any
3939
let hasCompareTarget = false
4040
let scheduled = false
41-
this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
41+
this.effect = new ReactiveEffect(getter, (_c, computedTrigger?: boolean) => {
4242
if (this.dep) {
4343
if (computedTrigger) {
4444
compareTarget = this._value
@@ -49,7 +49,7 @@ class DeferredComputedRefImpl<T> {
4949
hasCompareTarget = false
5050
scheduler(() => {
5151
if (this.effect.active && this._get() !== valueToCompare) {
52-
triggerRefValue(this)
52+
triggerRefValue(this, undefined)
5353
}
5454
scheduled = false
5555
})
@@ -59,7 +59,7 @@ class DeferredComputedRefImpl<T> {
5959
// deferred to be triggered in scheduler.
6060
for (const e of this.dep) {
6161
if (e.computed instanceof DeferredComputedRefImpl) {
62-
e.scheduler!(true /* computedTrigger */)
62+
e.scheduler!(undefined, true /* computedTrigger */)
6363
}
6464
}
6565
}

packages/reactivity/src/effect.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export let trackOpBit = 1
3030
*/
3131
const maxMarkerBits = 30
3232

33-
export type EffectScheduler = (...args: any[]) => any
33+
export type EffectScheduler = (computedToAskDirty: ComputedRefImpl<any> | undefined, ...args: any[]) => any
3434

3535
export type DebuggerEvent = {
3636
effect: ReactiveEffect
@@ -259,6 +259,7 @@ export function trackEffects(
259259
export function trigger(
260260
target: object,
261261
type: TriggerOpTypes,
262+
computedToAskDirty: ComputedRefImpl<any> | undefined,
262263
key?: unknown,
263264
newValue?: unknown,
264265
oldValue?: unknown,
@@ -323,9 +324,9 @@ export function trigger(
323324
if (deps.length === 1) {
324325
if (deps[0]) {
325326
if (__DEV__) {
326-
triggerEffects(deps[0], eventInfo)
327+
triggerEffects(deps[0], computedToAskDirty, eventInfo)
327328
} else {
328-
triggerEffects(deps[0])
329+
triggerEffects(deps[0], computedToAskDirty)
329330
}
330331
}
331332
} else {
@@ -336,41 +337,36 @@ export function trigger(
336337
}
337338
}
338339
if (__DEV__) {
339-
triggerEffects(createDep(effects), eventInfo)
340+
triggerEffects(createDep(effects), computedToAskDirty, eventInfo)
340341
} else {
341-
triggerEffects(createDep(effects))
342+
triggerEffects(createDep(effects), computedToAskDirty)
342343
}
343344
}
344345
}
345346

346347
export function triggerEffects(
347348
dep: Dep | ReactiveEffect[],
349+
computedToAskDirty: ComputedRefImpl<any> | undefined,
348350
debuggerEventExtraInfo?: DebuggerEventExtraInfo
349351
) {
350352
// spread into array for stabilization
351353
const effects = isArray(dep) ? dep : [...dep]
352354
for (const effect of effects) {
353-
if (effect.computed) {
354-
triggerEffect(effect, debuggerEventExtraInfo)
355-
}
356-
}
357-
for (const effect of effects) {
358-
if (!effect.computed) {
359-
triggerEffect(effect, debuggerEventExtraInfo)
360-
}
355+
triggerEffect(effect, computedToAskDirty, debuggerEventExtraInfo)
361356
}
362357
}
363358

364359
function triggerEffect(
365360
effect: ReactiveEffect,
361+
computedToAskDirty: ComputedRefImpl<any> | undefined,
366362
debuggerEventExtraInfo?: DebuggerEventExtraInfo
367363
) {
368364
if (effect !== activeEffect || effect.allowRecurse) {
369365
if (__DEV__ && effect.onTrigger) {
370366
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
371367
}
372368
if (effect.scheduler) {
373-
effect.scheduler()
369+
effect.scheduler(computedToAskDirty)
374370
} else {
375371
effect.run()
376372
}

packages/reactivity/src/ref.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { isProxy, toRaw, isReactive, toReactive } from './reactive'
1010
import type { ShallowReactiveMarker } from './reactive'
1111
import { CollectionTypes } from './collectionHandlers'
1212
import { createDep, Dep } from './dep'
13+
import { ComputedRefImpl } from './computed'
1314

1415
declare const RefSymbol: unique symbol
1516
export declare const RawSymbol: unique symbol
@@ -44,18 +45,18 @@ export function trackRefValue(ref: RefBase<any>) {
4445
}
4546
}
4647

47-
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
48+
export function triggerRefValue(ref: RefBase<any>, computedToAskDirty: ComputedRefImpl<any> | undefined, newVal?: any) {
4849
ref = toRaw(ref)
4950
if (ref.dep) {
5051
if (__DEV__) {
51-
triggerEffects(ref.dep, {
52+
triggerEffects(ref.dep, computedToAskDirty, {
5253
target: ref,
5354
type: TriggerOpTypes.SET,
5455
key: 'value',
5556
newValue: newVal
5657
})
5758
} else {
58-
triggerEffects(ref.dep)
59+
triggerEffects(ref.dep, computedToAskDirty)
5960
}
6061
}
6162
}
@@ -116,7 +117,7 @@ class RefImpl<T> {
116117
if (hasChanged(newVal, this._rawValue)) {
117118
this._rawValue = newVal
118119
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
119-
triggerRefValue(this, newVal)
120+
triggerRefValue(this, undefined, newVal)
120121
}
121122
}
122123
}
@@ -169,7 +170,7 @@ class CustomRefImpl<T> {
169170
constructor(factory: CustomRefFactory<T>) {
170171
const { get, set } = factory(
171172
() => trackRefValue(this),
172-
() => triggerRefValue(this)
173+
() => triggerRefValue(this, undefined)
173174
)
174175
this._get = get
175176
this._set = set

packages/runtime-core/src/compat/global.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ function defineReactiveSimple(obj: any, key: string, val: any) {
647647
},
648648
set(newVal) {
649649
val = isObject(newVal) ? reactive(newVal) : newVal
650-
trigger(obj, TriggerOpTypes.SET, key, newVal)
650+
trigger(obj, TriggerOpTypes.SET, undefined, key, newVal)
651651
}
652652
})
653653
}

packages/runtime-core/src/componentProps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ export function updateProps(
322322

323323
// trigger updates for $attrs in case it's used in component slots
324324
if (hasAttrsChanged) {
325-
trigger(instance, TriggerOpTypes.SET, '$attrs')
325+
trigger(instance, TriggerOpTypes.SET, undefined, '$attrs')
326326
}
327327

328328
if (__DEV__) {

0 commit comments

Comments
 (0)