Skip to content

Commit 313dc61

Browse files
perf(reactivity): refactor reactivity core by porting alien-signals (#12349)
1 parent 6eb29d3 commit 313dc61

16 files changed

+866
-777
lines changed

packages/reactivity/__tests__/computed.spec.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ import {
2525
toRaw,
2626
triggerRef,
2727
} from '../src'
28-
import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
2928
import type { ComputedRef, ComputedRefImpl } from '../src/computed'
29+
import { pauseTracking, resetTracking } from '../src/effect'
30+
import { SubscriberFlags } from '../src/system'
3031

3132
describe('reactivity/computed', () => {
3233
it('should return updated value', () => {
@@ -409,9 +410,9 @@ describe('reactivity/computed', () => {
409410
a.value++
410411
e.value
411412

412-
expect(e.deps!.dep).toBe(b.dep)
413-
expect(e.deps!.nextDep!.dep).toBe(d.dep)
414-
expect(e.deps!.nextDep!.nextDep!.dep).toBe(c.dep)
413+
expect(e.deps!.dep).toBe(b)
414+
expect(e.deps!.nextDep!.dep).toBe(d)
415+
expect(e.deps!.nextDep!.nextDep!.dep).toBe(c)
415416
expect(cSpy).toHaveBeenCalledTimes(2)
416417

417418
a.value++
@@ -466,8 +467,8 @@ describe('reactivity/computed', () => {
466467
const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
467468

468469
c2.value
469-
expect(c1.flags & EffectFlags.DIRTY).toBeFalsy()
470-
expect(c2.flags & EffectFlags.DIRTY).toBeFalsy()
470+
expect(c1.flags & SubscriberFlags.Dirtys).toBe(0)
471+
expect(c2.flags & SubscriberFlags.Dirtys).toBe(0)
471472
})
472473

473474
it('should chained computeds dirtyLevel update with first computed effect', () => {

packages/reactivity/__tests__/effect.spec.ts

+14-19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
import {
2+
computed,
3+
h,
4+
nextTick,
5+
nodeOps,
6+
ref,
7+
render,
8+
serializeInner,
9+
} from '@vue/runtime-test'
10+
import { ITERATE_KEY, getDepFromReactive } from '../src/dep'
11+
import { onEffectCleanup, pauseTracking, resetTracking } from '../src/effect'
112
import {
213
type DebuggerEvent,
314
type ReactiveEffectRunner,
@@ -11,23 +22,7 @@ import {
1122
stop,
1223
toRaw,
1324
} from '../src/index'
14-
import { type Dep, ITERATE_KEY, getDepFromReactive } from '../src/dep'
15-
import {
16-
computed,
17-
h,
18-
nextTick,
19-
nodeOps,
20-
ref,
21-
render,
22-
serializeInner,
23-
} from '@vue/runtime-test'
24-
import {
25-
endBatch,
26-
onEffectCleanup,
27-
pauseTracking,
28-
resetTracking,
29-
startBatch,
30-
} from '../src/effect'
25+
import { type Dependency, endBatch, startBatch } from '../src/system'
3126

3227
describe('reactivity/effect', () => {
3328
it('should run the passed function once (wrapped by a effect)', () => {
@@ -1183,12 +1178,12 @@ describe('reactivity/effect', () => {
11831178
})
11841179

11851180
describe('dep unsubscribe', () => {
1186-
function getSubCount(dep: Dep | undefined) {
1181+
function getSubCount(dep: Dependency | undefined) {
11871182
let count = 0
11881183
let sub = dep!.subs
11891184
while (sub) {
11901185
count++
1191-
sub = sub.prevSub
1186+
sub = sub.nextSub
11921187
}
11931188
return count
11941189
}

packages/reactivity/__tests__/gc.spec.ts

+35-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
type ComputedRef,
33
computed,
44
effect,
5+
effectScope,
56
reactive,
67
shallowRef as ref,
78
toRaw,
@@ -19,7 +20,7 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
1920
}
2021

2122
// #9233
22-
it('should release computed cache', async () => {
23+
it.todo('should release computed cache', async () => {
2324
const src = ref<{} | undefined>({})
2425
// @ts-expect-error ES2021 API
2526
const srcRef = new WeakRef(src.value!)
@@ -34,7 +35,7 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
3435
expect(srcRef.deref()).toBeUndefined()
3536
})
3637

37-
it('should release reactive property dep', async () => {
38+
it.todo('should release reactive property dep', async () => {
3839
const src = reactive({ foo: 1 })
3940

4041
let c: ComputedRef | undefined = computed(() => src.foo)
@@ -79,4 +80,36 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
7980
src.foo++
8081
expect(spy).toHaveBeenCalledTimes(2)
8182
})
83+
84+
it('should release computed that untrack by effect', async () => {
85+
const src = ref(0)
86+
// @ts-expect-error ES2021 API
87+
const c = new WeakRef(computed(() => src.value))
88+
const scope = effectScope()
89+
90+
scope.run(() => {
91+
effect(() => c.deref().value)
92+
})
93+
94+
expect(c.deref()).toBeDefined()
95+
scope.stop()
96+
await gc()
97+
expect(c.deref()).toBeUndefined()
98+
})
99+
100+
it('should release computed that untrack by effectScope', async () => {
101+
const src = ref(0)
102+
// @ts-expect-error ES2021 API
103+
const c = new WeakRef(computed(() => src.value))
104+
const scope = effectScope()
105+
106+
scope.run(() => {
107+
c.deref().value
108+
})
109+
110+
expect(c.deref()).toBeDefined()
111+
scope.stop()
112+
await gc()
113+
expect(c.deref()).toBeUndefined()
114+
})
82115
})

packages/reactivity/src/arrayInstrumentations.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { isArray } from '@vue/shared'
12
import { TrackOpTypes } from './constants'
2-
import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
3-
import { isProxy, isShallow, toRaw, toReactive } from './reactive'
43
import { ARRAY_ITERATE_KEY, track } from './dep'
5-
import { isArray } from '@vue/shared'
4+
import { pauseTracking, resetTracking } from './effect'
5+
import { isProxy, isShallow, toRaw, toReactive } from './reactive'
6+
import { endBatch, startBatch } from './system'
67

78
/**
89
* Track array iteration and return:

packages/reactivity/src/computed.ts

+101-66
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
import { isFunction } from '@vue/shared'
1+
import { hasChanged, isFunction } from '@vue/shared'
2+
import { ReactiveFlags, TrackOpTypes } from './constants'
3+
import { onTrack, setupFlagsHandler } from './debug'
24
import {
35
type DebuggerEvent,
46
type DebuggerOptions,
5-
EffectFlags,
6-
type Subscriber,
77
activeSub,
8-
batch,
9-
refreshComputed,
8+
activeTrackId,
9+
nextTrackId,
10+
setActiveSub,
1011
} from './effect'
12+
import { activeEffectScope } from './effectScope'
1113
import type { Ref } from './ref'
14+
import {
15+
type Dependency,
16+
type IComputed,
17+
type Link,
18+
SubscriberFlags,
19+
checkDirty,
20+
endTrack,
21+
link,
22+
startTrack,
23+
} from './system'
1224
import { warn } from './warning'
13-
import { Dep, type Link, globalVersion } from './dep'
14-
import { ReactiveFlags, TrackOpTypes } from './constants'
1525

1626
declare const ComputedRefSymbol: unique symbol
1727
declare const WritableComputedRefSymbol: unique symbol
@@ -44,15 +54,23 @@ export interface WritableComputedOptions<T, S = T> {
4454
* @private exported by @vue/reactivity for Vue core use, but not exported from
4555
* the main vue package
4656
*/
47-
export class ComputedRefImpl<T = any> implements Subscriber {
57+
export class ComputedRefImpl<T = any> implements IComputed {
4858
/**
4959
* @internal
5060
*/
51-
_value: any = undefined
52-
/**
53-
* @internal
54-
*/
55-
readonly dep: Dep = new Dep(this)
61+
_value: T | undefined = undefined
62+
version = 0
63+
64+
// Dependency
65+
subs: Link | undefined = undefined
66+
subsTail: Link | undefined = undefined
67+
lastTrackedId = 0
68+
69+
// Subscriber
70+
deps: Link | undefined = undefined
71+
depsTail: Link | undefined = undefined
72+
flags: SubscriberFlags = SubscriberFlags.Dirty
73+
5674
/**
5775
* @internal
5876
*/
@@ -63,34 +81,39 @@ export class ComputedRefImpl<T = any> implements Subscriber {
6381
*/
6482
readonly __v_isReadonly: boolean
6583
// TODO isolatedDeclarations ReactiveFlags.IS_READONLY
66-
// A computed is also a subscriber that tracks other deps
67-
/**
68-
* @internal
69-
*/
70-
deps?: Link = undefined
71-
/**
72-
* @internal
73-
*/
74-
depsTail?: Link = undefined
75-
/**
76-
* @internal
77-
*/
78-
flags: EffectFlags = EffectFlags.DIRTY
79-
/**
80-
* @internal
81-
*/
82-
globalVersion: number = globalVersion - 1
83-
/**
84-
* @internal
85-
*/
86-
isSSR: boolean
87-
/**
88-
* @internal
89-
*/
90-
next?: Subscriber = undefined
9184

9285
// for backwards compat
93-
effect: this = this
86+
get effect(): this {
87+
return this
88+
}
89+
// for backwards compat
90+
get dep(): Dependency {
91+
return this
92+
}
93+
// for backwards compat
94+
get _dirty(): boolean {
95+
const flags = this.flags
96+
if (flags & SubscriberFlags.Dirty) {
97+
return true
98+
} else if (flags & SubscriberFlags.ToCheckDirty) {
99+
if (checkDirty(this.deps!)) {
100+
this.flags |= SubscriberFlags.Dirty
101+
return true
102+
} else {
103+
this.flags &= ~SubscriberFlags.ToCheckDirty
104+
return false
105+
}
106+
}
107+
return false
108+
}
109+
set _dirty(v: boolean) {
110+
if (v) {
111+
this.flags |= SubscriberFlags.Dirty
112+
} else {
113+
this.flags &= ~SubscriberFlags.Dirtys
114+
}
115+
}
116+
94117
// dev only
95118
onTrack?: (event: DebuggerEvent) => void
96119
// dev only
@@ -105,43 +128,34 @@ export class ComputedRefImpl<T = any> implements Subscriber {
105128
constructor(
106129
public fn: ComputedGetter<T>,
107130
private readonly setter: ComputedSetter<T> | undefined,
108-
isSSR: boolean,
109131
) {
110132
this[ReactiveFlags.IS_READONLY] = !setter
111-
this.isSSR = isSSR
112-
}
113-
114-
/**
115-
* @internal
116-
*/
117-
notify(): true | void {
118-
this.flags |= EffectFlags.DIRTY
119-
if (
120-
!(this.flags & EffectFlags.NOTIFIED) &&
121-
// avoid infinite self recursion
122-
activeSub !== this
123-
) {
124-
batch(this, true)
125-
return true
126-
} else if (__DEV__) {
127-
// TODO warn
133+
if (__DEV__) {
134+
setupFlagsHandler(this)
128135
}
129136
}
130137

131138
get value(): T {
132-
const link = __DEV__
133-
? this.dep.track({
139+
if (this._dirty) {
140+
this.update()
141+
}
142+
if (activeTrackId !== 0 && this.lastTrackedId !== activeTrackId) {
143+
if (__DEV__) {
144+
onTrack(activeSub!, {
134145
target: this,
135146
type: TrackOpTypes.GET,
136147
key: 'value',
137148
})
138-
: this.dep.track()
139-
refreshComputed(this)
140-
// sync version after evaluation
141-
if (link) {
142-
link.version = this.dep.version
149+
}
150+
this.lastTrackedId = activeTrackId
151+
link(this, activeSub!).version = this.version
152+
} else if (
153+
activeEffectScope !== undefined &&
154+
this.lastTrackedId !== activeEffectScope.trackId
155+
) {
156+
link(this, activeEffectScope)
143157
}
144-
return this._value
158+
return this._value!
145159
}
146160

147161
set value(newValue) {
@@ -151,6 +165,27 @@ export class ComputedRefImpl<T = any> implements Subscriber {
151165
warn('Write operation failed: computed value is readonly')
152166
}
153167
}
168+
169+
update(): boolean {
170+
const prevSub = activeSub
171+
const prevTrackId = activeTrackId
172+
setActiveSub(this, nextTrackId())
173+
startTrack(this)
174+
const oldValue = this._value
175+
let newValue: T
176+
try {
177+
newValue = this.fn(oldValue)
178+
} finally {
179+
setActiveSub(prevSub, prevTrackId)
180+
endTrack(this)
181+
}
182+
if (hasChanged(oldValue, newValue)) {
183+
this._value = newValue
184+
this.version++
185+
return true
186+
}
187+
return false
188+
}
154189
}
155190

156191
/**
@@ -209,7 +244,7 @@ export function computed<T>(
209244
setter = getterOrOptions.set
210245
}
211246

212-
const cRef = new ComputedRefImpl(getter, setter, isSSR)
247+
const cRef = new ComputedRefImpl(getter, setter)
213248

214249
if (__DEV__ && debugOptions && !isSSR) {
215250
cRef.onTrack = debugOptions.onTrack

0 commit comments

Comments
 (0)