Skip to content

Commit bfd33fa

Browse files
committed
feat(reactivity): effectScope API
1 parent ba1d97c commit bfd33fa

File tree

13 files changed

+472
-68
lines changed

13 files changed

+472
-68
lines changed
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import {
2+
reactive,
3+
effect,
4+
effectScope,
5+
stop,
6+
isEffectScope,
7+
extendScope,
8+
computed,
9+
ref,
10+
onScopeStopped
11+
} from '../src'
12+
13+
describe('reactivity/effect/scope', () => {
14+
it('should run the passed function once (wrapped by a effect)', () => {
15+
const fnSpy = jest.fn(() => {})
16+
effectScope(fnSpy)
17+
expect(fnSpy).toHaveBeenCalledTimes(1)
18+
})
19+
20+
it('should accept zero argument', () => {
21+
const scope = effectScope()
22+
expect(scope._scope.effects.length).toBe(0)
23+
})
24+
25+
it('should collect the effects', () => {
26+
const scope = effectScope(() => {
27+
let dummy
28+
const counter = reactive({ num: 0 })
29+
effect(() => (dummy = counter.num))
30+
31+
expect(dummy).toBe(0)
32+
counter.num = 7
33+
expect(dummy).toBe(7)
34+
})
35+
36+
expect(scope._scope.effects.length).toBe(1)
37+
})
38+
39+
it('stop', () => {
40+
let dummy, doubled
41+
const counter = reactive({ num: 0 })
42+
43+
const scope = effectScope(() => {
44+
effect(() => (dummy = counter.num))
45+
effect(() => (doubled = counter.num * 2))
46+
})
47+
48+
expect(scope._scope.effects.length).toBe(2)
49+
50+
expect(dummy).toBe(0)
51+
counter.num = 7
52+
expect(dummy).toBe(7)
53+
expect(doubled).toBe(14)
54+
55+
stop(scope)
56+
57+
counter.num = 6
58+
expect(dummy).toBe(7)
59+
expect(doubled).toBe(14)
60+
})
61+
62+
it('should collect nested scope', () => {
63+
let dummy, doubled
64+
const counter = reactive({ num: 0 })
65+
66+
const scope = effectScope(() => {
67+
effect(() => (dummy = counter.num))
68+
// nested scope
69+
effectScope(() => {
70+
effect(() => (doubled = counter.num * 2))
71+
})
72+
})
73+
74+
expect(scope._scope.effects.length).toBe(2)
75+
expect(isEffectScope(scope._scope.effects[1])).toBe(true)
76+
77+
expect(dummy).toBe(0)
78+
counter.num = 7
79+
expect(dummy).toBe(7)
80+
expect(doubled).toBe(14)
81+
82+
// stop the nested scope as well
83+
stop(scope)
84+
85+
counter.num = 6
86+
expect(dummy).toBe(7)
87+
expect(doubled).toBe(14)
88+
})
89+
90+
it('nested scope can be escaped', () => {
91+
let dummy, doubled
92+
const counter = reactive({ num: 0 })
93+
94+
const scope = effectScope(() => {
95+
effect(() => (dummy = counter.num))
96+
// nested scope
97+
effectScope(
98+
() => {
99+
effect(() => (doubled = counter.num * 2))
100+
},
101+
{ detached: true }
102+
)
103+
})
104+
105+
expect(scope._scope.effects.length).toBe(1)
106+
107+
expect(dummy).toBe(0)
108+
counter.num = 7
109+
expect(dummy).toBe(7)
110+
expect(doubled).toBe(14)
111+
112+
stop(scope)
113+
114+
counter.num = 6
115+
expect(dummy).toBe(7)
116+
117+
// nested scope should not be stoped
118+
expect(doubled).toBe(12)
119+
})
120+
121+
it('able to extend the scope', () => {
122+
let dummy, doubled
123+
const counter = reactive({ num: 0 })
124+
125+
const scope = effectScope(() => {
126+
effect(() => (dummy = counter.num))
127+
})
128+
129+
expect(scope._scope.effects.length).toBe(1)
130+
131+
extendScope(scope, () => {
132+
effect(() => (doubled = counter.num * 2))
133+
})
134+
135+
expect(scope._scope.effects.length).toBe(2)
136+
137+
counter.num = 7
138+
expect(dummy).toBe(7)
139+
expect(doubled).toBe(14)
140+
141+
stop(scope)
142+
})
143+
144+
it('can not extend an inactive scope', () => {
145+
let dummy, doubled
146+
const counter = reactive({ num: 0 })
147+
148+
const scope = effectScope(() => {
149+
effect(() => (dummy = counter.num))
150+
})
151+
152+
expect(scope._scope.effects.length).toBe(1)
153+
154+
stop(scope)
155+
156+
extendScope(scope, () => {
157+
effect(() => (doubled = counter.num * 2))
158+
})
159+
160+
expect('[Vue warn] can not extend on an inactive scope.').toHaveBeenWarned()
161+
162+
expect(scope._scope.effects.length).toBe(1)
163+
164+
counter.num = 7
165+
expect(dummy).toBe(0)
166+
expect(doubled).toBe(undefined)
167+
})
168+
169+
it('should fire onStop hook', () => {
170+
let dummy = 0
171+
172+
const scope = effectScope(onStop => {
173+
onStop(() => (dummy += 1))
174+
onStop(() => (dummy += 2))
175+
})
176+
177+
extendScope(scope, onStop => {
178+
onStop(() => (dummy += 4))
179+
})
180+
181+
expect(dummy).toBe(0)
182+
183+
stop(scope)
184+
expect(dummy).toBe(7)
185+
})
186+
187+
it('should fire onScopeStopped hook', () => {
188+
let dummy = 0
189+
190+
const scope = effectScope(() => {
191+
onScopeStopped(() => (dummy += 1))
192+
onScopeStopped(() => (dummy += 2))
193+
})
194+
195+
extendScope(scope, () => {
196+
onScopeStopped(() => (dummy += 4))
197+
})
198+
199+
expect(dummy).toBe(0)
200+
201+
stop(scope)
202+
expect(dummy).toBe(7)
203+
})
204+
205+
it('should forward returns', () => {
206+
let foo = ref(1)
207+
208+
const scope = effectScope(() => {
209+
return {
210+
doubled: computed(() => foo.value * 2)
211+
}
212+
})
213+
214+
const { doubled } = scope
215+
216+
expect(doubled.value).toBe(2)
217+
218+
foo.value += 1
219+
expect(doubled.value).toBe(4)
220+
221+
stop(scope)
222+
223+
foo.value += 1
224+
expect(doubled.value).toBe(4)
225+
})
226+
227+
it('should forward returns on extending', () => {
228+
let foo = ref(1)
229+
230+
const scope = effectScope()
231+
232+
const { tripled } = extendScope(scope, () => {
233+
return {
234+
tripled: computed(() => foo.value * 3)
235+
}
236+
})
237+
238+
expect(tripled.value).toBe(3)
239+
240+
foo.value += 1
241+
expect(tripled.value).toBe(6)
242+
243+
stop(scope)
244+
245+
foo.value += 1
246+
expect(tripled.value).toBe(6)
247+
})
248+
})

packages/reactivity/src/effect.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { TrackOpTypes, TriggerOpTypes } from './operations'
22
import { extend, isArray, isIntegerKey, isMap } from '@vue/shared'
3+
import {
4+
EffectScope,
5+
EffectScopeReturns,
6+
isEffectScope,
7+
isEffectScopeReturns,
8+
recordEffectScope
9+
} from './effectScope'
310

411
// The main WeakMap that stores {target -> key -> dep} connections.
512
// Conceptually, it's easier to think of a dependency as a Dep class
@@ -43,9 +50,12 @@ export class ReactiveEffect<T = any> {
4350
constructor(
4451
public fn: () => T,
4552
public scheduler: EffectScheduler | null = null,
53+
public scope?: EffectScope | null,
4654
// allow recursive self-invocation
4755
public allowRecurse = false
48-
) {}
56+
) {
57+
recordEffectScope(this, scope)
58+
}
4959

5060
run() {
5161
if (!this.active) {
@@ -90,6 +100,7 @@ export class ReactiveEffect<T = any> {
90100
export interface ReactiveEffectOptions {
91101
lazy?: boolean
92102
scheduler?: EffectScheduler
103+
scope?: EffectScope
93104
allowRecurse?: boolean
94105
onStop?: () => void
95106
onTrack?: (event: DebuggerEvent) => void
@@ -112,6 +123,7 @@ export function effect<T = any>(
112123
const _effect = new ReactiveEffect(fn)
113124
if (options) {
114125
extend(_effect, options)
126+
if (options.scope) recordEffectScope(_effect, options.scope)
115127
}
116128
if (!options || !options.lazy) {
117129
_effect.run()
@@ -121,8 +133,25 @@ export function effect<T = any>(
121133
return runner
122134
}
123135

124-
export function stop(runner: ReactiveEffectRunner) {
125-
runner.effect.stop()
136+
export function stop(
137+
runner:
138+
| ReactiveEffect
139+
| ReactiveEffectRunner
140+
| EffectScope
141+
| EffectScopeReturns
142+
) {
143+
if (isEffectScopeReturns(runner)) {
144+
runner = runner._scope
145+
}
146+
if (isEffectScope(runner)) {
147+
runner.effects.forEach(stop)
148+
runner.onStopHooks.forEach(e => e(runner as EffectScope))
149+
runner.active = false
150+
} else if ('effect' in runner) {
151+
runner.effect.stop()
152+
} else {
153+
runner.stop()
154+
}
126155
}
127156

128157
let shouldTrack = true

0 commit comments

Comments
 (0)