-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(reactivity): new effectScope API (#2195)
- Loading branch information
Showing
16 changed files
with
400 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
import { nextTick, watch, watchEffect } from '@vue/runtime-core' | ||
import { | ||
reactive, | ||
effect, | ||
EffectScope, | ||
onScopeDispose, | ||
computed, | ||
ref, | ||
ComputedRef | ||
} from '../src' | ||
|
||
describe('reactivity/effect/scope', () => { | ||
it('should run', () => { | ||
const fnSpy = jest.fn(() => {}) | ||
new EffectScope().run(fnSpy) | ||
expect(fnSpy).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it('should accept zero argument', () => { | ||
const scope = new EffectScope() | ||
expect(scope.effects.length).toBe(0) | ||
}) | ||
|
||
it('should return run value', () => { | ||
expect(new EffectScope().run(() => 1)).toBe(1) | ||
}) | ||
|
||
it('should collect the effects', () => { | ||
const scope = new EffectScope() | ||
scope.run(() => { | ||
let dummy | ||
const counter = reactive({ num: 0 }) | ||
effect(() => (dummy = counter.num)) | ||
|
||
expect(dummy).toBe(0) | ||
counter.num = 7 | ||
expect(dummy).toBe(7) | ||
}) | ||
|
||
expect(scope.effects.length).toBe(1) | ||
}) | ||
|
||
it('stop', () => { | ||
let dummy, doubled | ||
const counter = reactive({ num: 0 }) | ||
|
||
const scope = new EffectScope() | ||
scope.run(() => { | ||
effect(() => (dummy = counter.num)) | ||
effect(() => (doubled = counter.num * 2)) | ||
}) | ||
|
||
expect(scope.effects.length).toBe(2) | ||
|
||
expect(dummy).toBe(0) | ||
counter.num = 7 | ||
expect(dummy).toBe(7) | ||
expect(doubled).toBe(14) | ||
|
||
scope.stop() | ||
|
||
counter.num = 6 | ||
expect(dummy).toBe(7) | ||
expect(doubled).toBe(14) | ||
}) | ||
|
||
it('should collect nested scope', () => { | ||
let dummy, doubled | ||
const counter = reactive({ num: 0 }) | ||
|
||
const scope = new EffectScope() | ||
scope.run(() => { | ||
effect(() => (dummy = counter.num)) | ||
// nested scope | ||
new EffectScope().run(() => { | ||
effect(() => (doubled = counter.num * 2)) | ||
}) | ||
}) | ||
|
||
expect(scope.effects.length).toBe(2) | ||
expect(scope.effects[1]).toBeInstanceOf(EffectScope) | ||
|
||
expect(dummy).toBe(0) | ||
counter.num = 7 | ||
expect(dummy).toBe(7) | ||
expect(doubled).toBe(14) | ||
|
||
// stop the nested scope as well | ||
scope.stop() | ||
|
||
counter.num = 6 | ||
expect(dummy).toBe(7) | ||
expect(doubled).toBe(14) | ||
}) | ||
|
||
it('nested scope can be escaped', () => { | ||
let dummy, doubled | ||
const counter = reactive({ num: 0 }) | ||
|
||
const scope = new EffectScope() | ||
scope.run(() => { | ||
effect(() => (dummy = counter.num)) | ||
// nested scope | ||
new EffectScope(true).run(() => { | ||
effect(() => (doubled = counter.num * 2)) | ||
}) | ||
}) | ||
|
||
expect(scope.effects.length).toBe(1) | ||
|
||
expect(dummy).toBe(0) | ||
counter.num = 7 | ||
expect(dummy).toBe(7) | ||
expect(doubled).toBe(14) | ||
|
||
scope.stop() | ||
|
||
counter.num = 6 | ||
expect(dummy).toBe(7) | ||
|
||
// nested scope should not be stoped | ||
expect(doubled).toBe(12) | ||
}) | ||
|
||
it('able to run the scope', () => { | ||
let dummy, doubled | ||
const counter = reactive({ num: 0 }) | ||
|
||
const scope = new EffectScope() | ||
scope.run(() => { | ||
effect(() => (dummy = counter.num)) | ||
}) | ||
|
||
expect(scope.effects.length).toBe(1) | ||
|
||
scope.run(() => { | ||
effect(() => (doubled = counter.num * 2)) | ||
}) | ||
|
||
expect(scope.effects.length).toBe(2) | ||
|
||
counter.num = 7 | ||
expect(dummy).toBe(7) | ||
expect(doubled).toBe(14) | ||
|
||
scope.stop() | ||
}) | ||
|
||
it('can not run an inactive scope', () => { | ||
let dummy, doubled | ||
const counter = reactive({ num: 0 }) | ||
|
||
const scope = new EffectScope() | ||
scope.run(() => { | ||
effect(() => (dummy = counter.num)) | ||
}) | ||
|
||
expect(scope.effects.length).toBe(1) | ||
|
||
scope.stop() | ||
|
||
scope.run(() => { | ||
effect(() => (doubled = counter.num * 2)) | ||
}) | ||
|
||
expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned() | ||
|
||
expect(scope.effects.length).toBe(1) | ||
|
||
counter.num = 7 | ||
expect(dummy).toBe(0) | ||
expect(doubled).toBe(undefined) | ||
}) | ||
|
||
it('should fire onDispose hook', () => { | ||
let dummy = 0 | ||
|
||
const scope = new EffectScope() | ||
scope.run(() => { | ||
onScopeDispose(() => (dummy += 1)) | ||
onScopeDispose(() => (dummy += 2)) | ||
}) | ||
|
||
scope.run(() => { | ||
onScopeDispose(() => (dummy += 4)) | ||
}) | ||
|
||
expect(dummy).toBe(0) | ||
|
||
scope.stop() | ||
expect(dummy).toBe(7) | ||
}) | ||
|
||
it('test with higher level APIs', async () => { | ||
const r = ref(1) | ||
|
||
const computedSpy = jest.fn() | ||
const watchSpy = jest.fn() | ||
const watchEffectSpy = jest.fn() | ||
|
||
let c: ComputedRef | ||
const scope = new EffectScope() | ||
scope.run(() => { | ||
c = computed(() => { | ||
computedSpy() | ||
return r.value + 1 | ||
}) | ||
|
||
watch(r, watchSpy) | ||
watchEffect(() => { | ||
watchEffectSpy() | ||
r.value | ||
}) | ||
}) | ||
|
||
c!.value // computed is lazy so trigger collection | ||
expect(computedSpy).toHaveBeenCalledTimes(1) | ||
expect(watchSpy).toHaveBeenCalledTimes(0) | ||
expect(watchEffectSpy).toHaveBeenCalledTimes(1) | ||
|
||
r.value++ | ||
c!.value | ||
await nextTick() | ||
expect(computedSpy).toHaveBeenCalledTimes(2) | ||
expect(watchSpy).toHaveBeenCalledTimes(1) | ||
expect(watchEffectSpy).toHaveBeenCalledTimes(2) | ||
|
||
scope.stop() | ||
|
||
r.value++ | ||
c!.value | ||
await nextTick() | ||
// should not trigger anymore | ||
expect(computedSpy).toHaveBeenCalledTimes(2) | ||
expect(watchSpy).toHaveBeenCalledTimes(1) | ||
expect(watchEffectSpy).toHaveBeenCalledTimes(2) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { ReactiveEffect } from './effect' | ||
import { warn } from './warning' | ||
|
||
let activeEffectScope: EffectScope | undefined | ||
const effectScopeStack: EffectScope[] = [] | ||
|
||
export class EffectScope { | ||
active = true | ||
effects: (ReactiveEffect | EffectScope)[] = [] | ||
cleanups: (() => void)[] = [] | ||
|
||
constructor(detached = false) { | ||
if (!detached) { | ||
recordEffectScope(this) | ||
} | ||
} | ||
|
||
run<T>(fn: () => T): T | undefined { | ||
if (this.active) { | ||
try { | ||
this.on() | ||
return fn() | ||
} finally { | ||
this.off() | ||
} | ||
} else if (__DEV__) { | ||
warn(`cannot run an inactive effect scope.`) | ||
} | ||
} | ||
|
||
on() { | ||
if (this.active) { | ||
effectScopeStack.push(this) | ||
activeEffectScope = this | ||
} | ||
} | ||
|
||
off() { | ||
if (this.active) { | ||
effectScopeStack.pop() | ||
activeEffectScope = effectScopeStack[effectScopeStack.length - 1] | ||
} | ||
} | ||
|
||
stop() { | ||
if (this.active) { | ||
this.effects.forEach(e => e.stop()) | ||
this.cleanups.forEach(cleanup => cleanup()) | ||
this.active = false | ||
} | ||
} | ||
} | ||
|
||
export function effectScope(detached?: boolean) { | ||
return new EffectScope(detached) | ||
} | ||
|
||
export function recordEffectScope( | ||
effect: ReactiveEffect | EffectScope, | ||
scope?: EffectScope | null | ||
) { | ||
scope = scope || activeEffectScope | ||
if (scope && scope.active) { | ||
scope.effects.push(effect) | ||
} | ||
} | ||
|
||
export function getCurrentScope() { | ||
return activeEffectScope | ||
} | ||
|
||
export function onScopeDispose(fn: () => void) { | ||
if (activeEffectScope) { | ||
activeEffectScope.cleanups.push(fn) | ||
} else if (__DEV__) { | ||
warn( | ||
`onDispose() is called when there is no active effect scope ` + | ||
` to be associated with.` | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export function warn(msg: string, ...args: any[]) { | ||
console.warn(`[Vue warn] ${msg}`, ...args) | ||
} |
Oops, something went wrong.