Skip to content

Commit 6431040

Browse files
basvanmeursyyx990803
authored andcommitted
perf(reactivity): ref-specific track/trigger and miscellaneous optimizations (#3995)
1 parent ceff899 commit 6431040

File tree

4 files changed

+145
-61
lines changed

4 files changed

+145
-61
lines changed

packages/reactivity/src/computed.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { effect, ReactiveEffect, trigger, track } from './effect'
2-
import { TriggerOpTypes, TrackOpTypes } from './operations'
3-
import { Ref } from './ref'
1+
import { effect, ReactiveEffect } from './effect'
2+
import { Ref, trackRefValue, triggerRefValue } from './ref'
43
import { isFunction, NOOP } from '@vue/shared'
54
import { ReactiveFlags, toRaw } from './reactive'
65

@@ -21,6 +20,8 @@ export interface WritableComputedOptions<T> {
2120
}
2221

2322
class ComputedRefImpl<T> {
23+
public dep?: Set<ReactiveEffect> = undefined
24+
2425
private _value!: T
2526
private _dirty = true
2627

@@ -39,7 +40,7 @@ class ComputedRefImpl<T> {
3940
scheduler: () => {
4041
if (!this._dirty) {
4142
this._dirty = true
42-
trigger(toRaw(this), TriggerOpTypes.SET, 'value')
43+
triggerRefValue(this)
4344
}
4445
}
4546
})
@@ -54,7 +55,7 @@ class ComputedRefImpl<T> {
5455
self._value = this.effect()
5556
self._dirty = false
5657
}
57-
track(self, TrackOpTypes.GET, 'value')
58+
trackRefValue(this)
5859
return self._value
5960
}
6061

packages/reactivity/src/effect.ts

+84-45
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ export interface ReactiveEffectOptions {
4646

4747
export type DebuggerEvent = {
4848
effect: ReactiveEffect
49+
} & DebuggerEventExtraInfo
50+
51+
export type DebuggerEventExtraInfo = {
4952
target: object
5053
type: TrackOpTypes | TriggerOpTypes
5154
key: any
52-
} & DebuggerEventExtraInfo
53-
54-
export interface DebuggerEventExtraInfo {
5555
newValue?: any
5656
oldValue?: any
5757
oldTarget?: Map<any, any> | Set<any>
@@ -111,7 +111,8 @@ function createReactiveEffect<T = any>(
111111
} finally {
112112
effectStack.pop()
113113
resetTracking()
114-
activeEffect = effectStack[effectStack.length - 1]
114+
const n = effectStack.length
115+
activeEffect = n > 0 ? effectStack[n - 1] : undefined
115116
}
116117
}
117118
} as ReactiveEffect
@@ -154,7 +155,7 @@ export function resetTracking() {
154155
}
155156

156157
export function track(target: object, type: TrackOpTypes, key: unknown) {
157-
if (!shouldTrack || activeEffect === undefined) {
158+
if (!isTracking()) {
158159
return
159160
}
160161
let depsMap = targetMap.get(target)
@@ -165,16 +166,34 @@ export function track(target: object, type: TrackOpTypes, key: unknown) {
165166
if (!dep) {
166167
depsMap.set(key, (dep = new Set()))
167168
}
168-
if (!dep.has(activeEffect)) {
169-
dep.add(activeEffect)
170-
activeEffect.deps.push(dep)
171-
if (__DEV__ && activeEffect.options.onTrack) {
172-
activeEffect.options.onTrack({
173-
effect: activeEffect,
174-
target,
175-
type,
176-
key
177-
})
169+
170+
const eventInfo = __DEV__
171+
? { effect: activeEffect, target, type, key }
172+
: undefined
173+
174+
trackEffects(dep, eventInfo)
175+
}
176+
177+
export function isTracking() {
178+
return shouldTrack && activeEffect !== undefined
179+
}
180+
181+
export function trackEffects(
182+
dep: Set<ReactiveEffect>,
183+
debuggerEventExtraInfo?: DebuggerEventExtraInfo
184+
) {
185+
if (!dep.has(activeEffect!)) {
186+
dep.add(activeEffect!)
187+
activeEffect!.deps.push(dep)
188+
if (__DEV__ && activeEffect!.options.onTrack) {
189+
activeEffect!.options.onTrack(
190+
Object.assign(
191+
{
192+
effect: activeEffect!
193+
},
194+
debuggerEventExtraInfo
195+
)
196+
)
178197
}
179198
}
180199
}
@@ -193,73 +212,88 @@ export function trigger(
193212
return
194213
}
195214

196-
const effects = new Set<ReactiveEffect>()
197-
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
198-
if (effectsToAdd) {
199-
effectsToAdd.forEach(effect => {
200-
if (effect !== activeEffect || effect.allowRecurse) {
201-
effects.add(effect)
202-
}
203-
})
204-
}
205-
}
206-
215+
let sets: DepSets = []
207216
if (type === TriggerOpTypes.CLEAR) {
208217
// collection being cleared
209218
// trigger all effects for target
210-
depsMap.forEach(add)
219+
sets = [...depsMap.values()]
211220
} else if (key === 'length' && isArray(target)) {
212221
depsMap.forEach((dep, key) => {
213222
if (key === 'length' || key >= (newValue as number)) {
214-
add(dep)
223+
sets.push(dep)
215224
}
216225
})
217226
} else {
218227
// schedule runs for SET | ADD | DELETE
219228
if (key !== void 0) {
220-
add(depsMap.get(key))
229+
sets.push(depsMap.get(key))
221230
}
222231

223232
// also run for iteration key on ADD | DELETE | Map.SET
224233
switch (type) {
225234
case TriggerOpTypes.ADD:
226235
if (!isArray(target)) {
227-
add(depsMap.get(ITERATE_KEY))
236+
sets.push(depsMap.get(ITERATE_KEY))
228237
if (isMap(target)) {
229-
add(depsMap.get(MAP_KEY_ITERATE_KEY))
238+
sets.push(depsMap.get(MAP_KEY_ITERATE_KEY))
230239
}
231240
} else if (isIntegerKey(key)) {
232241
// new index added to array -> length changes
233-
add(depsMap.get('length'))
242+
sets.push(depsMap.get('length'))
234243
}
235244
break
236245
case TriggerOpTypes.DELETE:
237246
if (!isArray(target)) {
238-
add(depsMap.get(ITERATE_KEY))
247+
sets.push(depsMap.get(ITERATE_KEY))
239248
if (isMap(target)) {
240-
add(depsMap.get(MAP_KEY_ITERATE_KEY))
249+
sets.push(depsMap.get(MAP_KEY_ITERATE_KEY))
241250
}
242251
}
243252
break
244253
case TriggerOpTypes.SET:
245254
if (isMap(target)) {
246-
add(depsMap.get(ITERATE_KEY))
255+
sets.push(depsMap.get(ITERATE_KEY))
247256
}
248257
break
249258
}
250259
}
251260

261+
const eventInfo = __DEV__
262+
? { target, type, key, newValue, oldValue, oldTarget }
263+
: undefined
264+
triggerMultiEffects(sets, eventInfo)
265+
}
266+
267+
type DepSets = (Dep | undefined)[]
268+
269+
export function triggerMultiEffects(
270+
depSets: DepSets,
271+
debuggerEventExtraInfo?: DebuggerEventExtraInfo
272+
) {
273+
if (depSets.length === 1) {
274+
if (depSets[0]) {
275+
triggerEffects(depSets[0], debuggerEventExtraInfo)
276+
}
277+
} else {
278+
const sets = depSets.filter(s => !!s) as Dep[]
279+
triggerEffects(concatSets(sets), debuggerEventExtraInfo)
280+
}
281+
}
282+
283+
function concatSets<T>(sets: Set<T>[]): Set<T> {
284+
const all = ([] as T[]).concat(...sets.map(s => [...s!]))
285+
return new Set(all)
286+
}
287+
288+
export function triggerEffects(
289+
dep: Dep,
290+
debuggerEventExtraInfo?: DebuggerEventExtraInfo
291+
) {
252292
const run = (effect: ReactiveEffect) => {
253293
if (__DEV__ && effect.options.onTrigger) {
254-
effect.options.onTrigger({
255-
effect,
256-
target,
257-
key,
258-
type,
259-
newValue,
260-
oldValue,
261-
oldTarget
262-
})
294+
effect.options.onTrigger(
295+
Object.assign({ effect }, debuggerEventExtraInfo)
296+
)
263297
}
264298
if (effect.options.scheduler) {
265299
effect.options.scheduler(effect)
@@ -268,5 +302,10 @@ export function trigger(
268302
}
269303
}
270304

271-
effects.forEach(run)
305+
const immutableDeps = [...dep]
306+
immutableDeps.forEach(effect => {
307+
if (effect !== activeEffect || effect.allowRecurse) {
308+
run(effect)
309+
}
310+
})
272311
}

packages/reactivity/src/reactive.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,8 @@ export function isProxy(value: unknown): boolean {
225225
}
226226

227227
export function toRaw<T>(observed: T): T {
228-
return (
229-
(observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed
230-
)
228+
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
229+
return raw ? toRaw(raw) : observed
231230
}
232231

233232
export function markRaw<T extends object>(value: T): T {

packages/reactivity/src/ref.ts

+53-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { track, trigger } from './effect'
1+
import {
2+
isTracking,
3+
ReactiveEffect,
4+
trackEffects,
5+
triggerEffects
6+
} from './effect'
27
import { TrackOpTypes, TriggerOpTypes } from './operations'
38
import { isArray, isObject, hasChanged } from '@vue/shared'
49
import { reactive, isProxy, toRaw, isReactive } from './reactive'
@@ -18,6 +23,44 @@ export interface Ref<T = any> {
1823
* @internal
1924
*/
2025
_shallow?: boolean
26+
27+
/**
28+
* Deps are maintained locally rather than in depsMap for performance reasons.
29+
*/
30+
dep?: Set<ReactiveEffect>
31+
}
32+
33+
type RefBase<T> = {
34+
dep?: Set<ReactiveEffect>
35+
value: T
36+
}
37+
38+
export function trackRefValue(ref: RefBase<any>) {
39+
if (isTracking()) {
40+
ref = toRaw(ref)
41+
const eventInfo = __DEV__
42+
? { target: ref, type: TrackOpTypes.GET, key: 'value' }
43+
: undefined
44+
if (!ref.dep) {
45+
ref.dep = new Set<ReactiveEffect>()
46+
}
47+
trackEffects(ref.dep, eventInfo)
48+
}
49+
}
50+
51+
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
52+
ref = toRaw(ref)
53+
if (ref.dep) {
54+
const eventInfo = __DEV__
55+
? {
56+
target: ref,
57+
type: TriggerOpTypes.SET,
58+
key: 'value',
59+
newValue: newVal
60+
}
61+
: undefined
62+
triggerEffects(ref.dep, eventInfo)
63+
}
2164
}
2265

2366
export type ToRef<T> = [T] extends [Ref] ? T : Ref<UnwrapRef<T>>
@@ -52,10 +95,10 @@ export function shallowRef(value?: unknown) {
5295
}
5396

5497
class RefImpl<T> {
55-
private _rawValue: T
56-
5798
private _value: T
99+
private _rawValue: T
58100

101+
public dep?: Set<ReactiveEffect> = undefined
59102
public readonly __v_isRef = true
60103

61104
constructor(value: T, public readonly _shallow = false) {
@@ -64,7 +107,7 @@ class RefImpl<T> {
64107
}
65108

66109
get value() {
67-
track(toRaw(this), TrackOpTypes.GET, 'value')
110+
trackRefValue(this)
68111
return this._value
69112
}
70113

@@ -73,7 +116,7 @@ class RefImpl<T> {
73116
if (hasChanged(newVal, this._rawValue)) {
74117
this._rawValue = newVal
75118
this._value = this._shallow ? newVal : convert(newVal)
76-
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
119+
triggerRefValue(this, newVal)
77120
}
78121
}
79122
}
@@ -86,7 +129,7 @@ function createRef(rawValue: unknown, shallow = false) {
86129
}
87130

88131
export function triggerRef(ref: Ref) {
89-
trigger(toRaw(ref), TriggerOpTypes.SET, 'value', __DEV__ ? ref.value : void 0)
132+
triggerRefValue(ref, __DEV__ ? ref.value : void 0)
90133
}
91134

92135
export function unref<T>(ref: T | Ref<T>): T {
@@ -123,15 +166,17 @@ export type CustomRefFactory<T> = (
123166
}
124167

125168
class CustomRefImpl<T> {
169+
public dep?: Set<ReactiveEffect> = undefined
170+
126171
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
127172
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
128173

129174
public readonly __v_isRef = true
130175

131176
constructor(factory: CustomRefFactory<T>) {
132177
const { get, set } = factory(
133-
() => track(this, TrackOpTypes.GET, 'value'),
134-
() => trigger(this, TriggerOpTypes.SET, 'value')
178+
() => trackRefValue(this),
179+
() => triggerRefValue(this)
135180
)
136181
this._get = get
137182
this._set = set

0 commit comments

Comments
 (0)