feat(reactivity): new effectScope API (#2195)
This commit is contained in:
parent
87f69fd0bb
commit
f5617fc3bb
238
packages/reactivity/__tests__/effectScope.spec.ts
Normal file
238
packages/reactivity/__tests__/effectScope.spec.ts
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
})
|
@ -1,5 +1,6 @@
|
|||||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||||
import { extend, isArray, isIntegerKey, isMap } from '@vue/shared'
|
import { extend, isArray, isIntegerKey, isMap } from '@vue/shared'
|
||||||
|
import { EffectScope, recordEffectScope } from './effectScope'
|
||||||
|
|
||||||
// The main WeakMap that stores {target -> key -> dep} connections.
|
// The main WeakMap that stores {target -> key -> dep} connections.
|
||||||
// Conceptually, it's easier to think of a dependency as a Dep class
|
// Conceptually, it's easier to think of a dependency as a Dep class
|
||||||
@ -43,9 +44,12 @@ export class ReactiveEffect<T = any> {
|
|||||||
constructor(
|
constructor(
|
||||||
public fn: () => T,
|
public fn: () => T,
|
||||||
public scheduler: EffectScheduler | null = null,
|
public scheduler: EffectScheduler | null = null,
|
||||||
|
scope?: EffectScope | null,
|
||||||
// allow recursive self-invocation
|
// allow recursive self-invocation
|
||||||
public allowRecurse = false
|
public allowRecurse = false
|
||||||
) {}
|
) {
|
||||||
|
recordEffectScope(this, scope)
|
||||||
|
}
|
||||||
|
|
||||||
run() {
|
run() {
|
||||||
if (!this.active) {
|
if (!this.active) {
|
||||||
@ -60,8 +64,7 @@ export class ReactiveEffect<T = any> {
|
|||||||
} finally {
|
} finally {
|
||||||
effectStack.pop()
|
effectStack.pop()
|
||||||
resetTracking()
|
resetTracking()
|
||||||
const n = effectStack.length
|
activeEffect = effectStack[effectStack.length - 1]
|
||||||
activeEffect = n > 0 ? effectStack[n - 1] : undefined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,6 +93,7 @@ export class ReactiveEffect<T = any> {
|
|||||||
export interface ReactiveEffectOptions {
|
export interface ReactiveEffectOptions {
|
||||||
lazy?: boolean
|
lazy?: boolean
|
||||||
scheduler?: EffectScheduler
|
scheduler?: EffectScheduler
|
||||||
|
scope?: EffectScope
|
||||||
allowRecurse?: boolean
|
allowRecurse?: boolean
|
||||||
onStop?: () => void
|
onStop?: () => void
|
||||||
onTrack?: (event: DebuggerEvent) => void
|
onTrack?: (event: DebuggerEvent) => void
|
||||||
@ -112,6 +116,7 @@ export function effect<T = any>(
|
|||||||
const _effect = new ReactiveEffect(fn)
|
const _effect = new ReactiveEffect(fn)
|
||||||
if (options) {
|
if (options) {
|
||||||
extend(_effect, options)
|
extend(_effect, options)
|
||||||
|
if (options.scope) recordEffectScope(_effect, options.scope)
|
||||||
}
|
}
|
||||||
if (!options || !options.lazy) {
|
if (!options || !options.lazy) {
|
||||||
_effect.run()
|
_effect.run()
|
||||||
|
81
packages/reactivity/src/effectScope.ts
Normal file
81
packages/reactivity/src/effectScope.ts
Normal file
@ -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.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -51,4 +51,10 @@ export {
|
|||||||
EffectScheduler,
|
EffectScheduler,
|
||||||
DebuggerEvent
|
DebuggerEvent
|
||||||
} from './effect'
|
} from './effect'
|
||||||
|
export {
|
||||||
|
effectScope,
|
||||||
|
EffectScope,
|
||||||
|
getCurrentScope,
|
||||||
|
onScopeDispose
|
||||||
|
} from './effectScope'
|
||||||
export { TrackOpTypes, TriggerOpTypes } from './operations'
|
export { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||||
|
3
packages/reactivity/src/warning.ts
Normal file
3
packages/reactivity/src/warning.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function warn(msg: string, ...args: any[]) {
|
||||||
|
console.warn(`[Vue warn] ${msg}`, ...args)
|
||||||
|
}
|
@ -848,15 +848,16 @@ describe('api: watch', () => {
|
|||||||
render(h(Comp), nodeOps.createElement('div'))
|
render(h(Comp), nodeOps.createElement('div'))
|
||||||
|
|
||||||
expect(instance!).toBeDefined()
|
expect(instance!).toBeDefined()
|
||||||
expect(instance!.effects).toBeInstanceOf(Array)
|
expect(instance!.scope.effects).toBeInstanceOf(Array)
|
||||||
expect(instance!.effects!.length).toBe(1)
|
// includes the component's own render effect AND the watcher effect
|
||||||
|
expect(instance!.scope.effects!.length).toBe(2)
|
||||||
|
|
||||||
_show!.value = false
|
_show!.value = false
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
expect(instance!.effects![0].active).toBe(false)
|
expect(instance!.scope.effects![0].active).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
|
test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import {
|
|
||||||
computed as _computed,
|
|
||||||
ComputedRef,
|
|
||||||
WritableComputedOptions,
|
|
||||||
WritableComputedRef,
|
|
||||||
ComputedGetter
|
|
||||||
} from '@vue/reactivity'
|
|
||||||
import { recordInstanceBoundEffect } from './component'
|
|
||||||
|
|
||||||
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
|
|
||||||
export function computed<T>(
|
|
||||||
options: WritableComputedOptions<T>
|
|
||||||
): WritableComputedRef<T>
|
|
||||||
export function computed<T>(
|
|
||||||
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
|
|
||||||
) {
|
|
||||||
const c = _computed(getterOrOptions as any)
|
|
||||||
recordInstanceBoundEffect(c.effect)
|
|
||||||
return c
|
|
||||||
}
|
|
@ -3,7 +3,8 @@ import {
|
|||||||
currentInstance,
|
currentInstance,
|
||||||
isInSSRComponentSetup,
|
isInSSRComponentSetup,
|
||||||
LifecycleHooks,
|
LifecycleHooks,
|
||||||
setCurrentInstance
|
setCurrentInstance,
|
||||||
|
unsetCurrentInstance
|
||||||
} from './component'
|
} from './component'
|
||||||
import { ComponentPublicInstance } from './componentPublicInstance'
|
import { ComponentPublicInstance } from './componentPublicInstance'
|
||||||
import { callWithAsyncErrorHandling, ErrorTypeStrings } from './errorHandling'
|
import { callWithAsyncErrorHandling, ErrorTypeStrings } from './errorHandling'
|
||||||
@ -38,7 +39,7 @@ export function injectHook(
|
|||||||
// can only be false when the user does something really funky.
|
// can only be false when the user does something really funky.
|
||||||
setCurrentInstance(target)
|
setCurrentInstance(target)
|
||||||
const res = callWithAsyncErrorHandling(hook, target, type, args)
|
const res = callWithAsyncErrorHandling(hook, target, type, args)
|
||||||
setCurrentInstance(null)
|
unsetCurrentInstance()
|
||||||
resetTracking()
|
resetTracking()
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
|
@ -3,7 +3,8 @@ import {
|
|||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
setCurrentInstance,
|
setCurrentInstance,
|
||||||
SetupContext,
|
SetupContext,
|
||||||
createSetupContext
|
createSetupContext,
|
||||||
|
unsetCurrentInstance
|
||||||
} from './component'
|
} from './component'
|
||||||
import { EmitFn, EmitsOptions } from './componentEmits'
|
import { EmitFn, EmitsOptions } from './componentEmits'
|
||||||
import {
|
import {
|
||||||
@ -248,9 +249,15 @@ export function mergeDefaults(
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function withAsyncContext(getAwaitable: () => any) {
|
export function withAsyncContext(getAwaitable: () => any) {
|
||||||
const ctx = getCurrentInstance()
|
const ctx = getCurrentInstance()!
|
||||||
|
if (__DEV__ && !ctx) {
|
||||||
|
warn(
|
||||||
|
`withAsyncContext called without active current instance. ` +
|
||||||
|
`This is likely a bug.`
|
||||||
|
)
|
||||||
|
}
|
||||||
let awaitable = getAwaitable()
|
let awaitable = getAwaitable()
|
||||||
setCurrentInstance(null)
|
unsetCurrentInstance()
|
||||||
if (isPromise(awaitable)) {
|
if (isPromise(awaitable)) {
|
||||||
awaitable = awaitable.catch(e => {
|
awaitable = awaitable.catch(e => {
|
||||||
setCurrentInstance(ctx)
|
setCurrentInstance(ctx)
|
||||||
|
@ -25,8 +25,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
currentInstance,
|
currentInstance,
|
||||||
ComponentInternalInstance,
|
ComponentInternalInstance,
|
||||||
isInSSRComponentSetup,
|
isInSSRComponentSetup
|
||||||
recordInstanceBoundEffect
|
|
||||||
} from './component'
|
} from './component'
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
@ -326,15 +325,14 @@ function doWatch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const effect = new ReactiveEffect(getter, scheduler)
|
const scope = instance && instance.scope
|
||||||
|
const effect = new ReactiveEffect(getter, scheduler, scope)
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
effect.onTrack = onTrack
|
effect.onTrack = onTrack
|
||||||
effect.onTrigger = onTrigger
|
effect.onTrigger = onTrigger
|
||||||
}
|
}
|
||||||
|
|
||||||
recordInstanceBoundEffect(effect, instance)
|
|
||||||
|
|
||||||
// initial run
|
// initial run
|
||||||
if (cb) {
|
if (cb) {
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
@ -353,8 +351,8 @@ function doWatch(
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
effect.stop()
|
effect.stop()
|
||||||
if (instance) {
|
if (scope) {
|
||||||
remove(instance.effects!, effect)
|
remove(scope.effects!, effect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -563,7 +563,7 @@ function installCompatMount(
|
|||||||
}
|
}
|
||||||
delete app._container.__vue_app__
|
delete app._container.__vue_app__
|
||||||
} else {
|
} else {
|
||||||
const { bum, effects, um } = instance
|
const { bum, scope, um } = instance
|
||||||
// beforeDestroy hooks
|
// beforeDestroy hooks
|
||||||
if (bum) {
|
if (bum) {
|
||||||
invokeArrayFns(bum)
|
invokeArrayFns(bum)
|
||||||
@ -572,10 +572,8 @@ function installCompatMount(
|
|||||||
instance.emit('hook:beforeDestroy')
|
instance.emit('hook:beforeDestroy')
|
||||||
}
|
}
|
||||||
// stop effects
|
// stop effects
|
||||||
if (effects) {
|
if (scope) {
|
||||||
for (let i = 0; i < effects.length; i++) {
|
scope.stop()
|
||||||
effects[i].stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// unmounted hook
|
// unmounted hook
|
||||||
if (um) {
|
if (um) {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { VNode, VNodeChild, isVNode } from './vnode'
|
import { VNode, VNodeChild, isVNode } from './vnode'
|
||||||
import {
|
import {
|
||||||
ReactiveEffect,
|
|
||||||
pauseTracking,
|
pauseTracking,
|
||||||
resetTracking,
|
resetTracking,
|
||||||
shallowReadonly,
|
shallowReadonly,
|
||||||
proxyRefs,
|
proxyRefs,
|
||||||
|
EffectScope,
|
||||||
markRaw
|
markRaw
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import {
|
import {
|
||||||
@ -217,11 +217,6 @@ export interface ComponentInternalInstance {
|
|||||||
* Root vnode of this component's own vdom tree
|
* Root vnode of this component's own vdom tree
|
||||||
*/
|
*/
|
||||||
subTree: VNode
|
subTree: VNode
|
||||||
/**
|
|
||||||
* Main update effect
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
effect: ReactiveEffect
|
|
||||||
/**
|
/**
|
||||||
* Bound effect runner to be passed to schedulers
|
* Bound effect runner to be passed to schedulers
|
||||||
*/
|
*/
|
||||||
@ -246,7 +241,7 @@ export interface ComponentInternalInstance {
|
|||||||
* so that they can be automatically stopped on component unmount
|
* so that they can be automatically stopped on component unmount
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
effects: ReactiveEffect[] | null
|
scope: EffectScope
|
||||||
/**
|
/**
|
||||||
* cache for proxy access type to avoid hasOwnProperty calls
|
* cache for proxy access type to avoid hasOwnProperty calls
|
||||||
* @internal
|
* @internal
|
||||||
@ -451,14 +446,13 @@ export function createComponentInstance(
|
|||||||
root: null!, // to be immediately set
|
root: null!, // to be immediately set
|
||||||
next: null,
|
next: null,
|
||||||
subTree: null!, // will be set synchronously right after creation
|
subTree: null!, // will be set synchronously right after creation
|
||||||
effect: null!, // will be set synchronously right after creation
|
|
||||||
update: null!, // will be set synchronously right after creation
|
update: null!, // will be set synchronously right after creation
|
||||||
|
scope: new EffectScope(),
|
||||||
render: null,
|
render: null,
|
||||||
proxy: null,
|
proxy: null,
|
||||||
exposed: null,
|
exposed: null,
|
||||||
exposeProxy: null,
|
exposeProxy: null,
|
||||||
withProxy: null,
|
withProxy: null,
|
||||||
effects: null,
|
|
||||||
provides: parent ? parent.provides : Object.create(appContext.provides),
|
provides: parent ? parent.provides : Object.create(appContext.provides),
|
||||||
accessCache: null!,
|
accessCache: null!,
|
||||||
renderCache: [],
|
renderCache: [],
|
||||||
@ -533,10 +527,14 @@ export let currentInstance: ComponentInternalInstance | null = null
|
|||||||
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
|
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
|
||||||
currentInstance || currentRenderingInstance
|
currentInstance || currentRenderingInstance
|
||||||
|
|
||||||
export const setCurrentInstance = (
|
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
|
||||||
instance: ComponentInternalInstance | null
|
|
||||||
) => {
|
|
||||||
currentInstance = instance
|
currentInstance = instance
|
||||||
|
instance.scope.on()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unsetCurrentInstance = () => {
|
||||||
|
currentInstance && currentInstance.scope.off()
|
||||||
|
currentInstance = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBuiltInTag = /*#__PURE__*/ makeMap('slot,component')
|
const isBuiltInTag = /*#__PURE__*/ makeMap('slot,component')
|
||||||
@ -618,7 +616,7 @@ function setupStatefulComponent(
|
|||||||
const setupContext = (instance.setupContext =
|
const setupContext = (instance.setupContext =
|
||||||
setup.length > 1 ? createSetupContext(instance) : null)
|
setup.length > 1 ? createSetupContext(instance) : null)
|
||||||
|
|
||||||
currentInstance = instance
|
setCurrentInstance(instance)
|
||||||
pauseTracking()
|
pauseTracking()
|
||||||
const setupResult = callWithErrorHandling(
|
const setupResult = callWithErrorHandling(
|
||||||
setup,
|
setup,
|
||||||
@ -627,13 +625,10 @@ function setupStatefulComponent(
|
|||||||
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
|
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
|
||||||
)
|
)
|
||||||
resetTracking()
|
resetTracking()
|
||||||
currentInstance = null
|
unsetCurrentInstance()
|
||||||
|
|
||||||
if (isPromise(setupResult)) {
|
if (isPromise(setupResult)) {
|
||||||
const unsetInstance = () => {
|
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
|
||||||
currentInstance = null
|
|
||||||
}
|
|
||||||
setupResult.then(unsetInstance, unsetInstance)
|
|
||||||
|
|
||||||
if (isSSR) {
|
if (isSSR) {
|
||||||
// return the promise so server-renderer can wait on it
|
// return the promise so server-renderer can wait on it
|
||||||
@ -801,11 +796,11 @@ export function finishComponentSetup(
|
|||||||
|
|
||||||
// support for 2.x options
|
// support for 2.x options
|
||||||
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
|
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
|
||||||
currentInstance = instance
|
setCurrentInstance(instance)
|
||||||
pauseTracking()
|
pauseTracking()
|
||||||
applyOptions(instance)
|
applyOptions(instance)
|
||||||
resetTracking()
|
resetTracking()
|
||||||
currentInstance = null
|
unsetCurrentInstance()
|
||||||
}
|
}
|
||||||
|
|
||||||
// warn missing template/render
|
// warn missing template/render
|
||||||
@ -900,17 +895,6 @@ export function getExposeProxy(instance: ComponentInternalInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// record effects created during a component's setup() so that they can be
|
|
||||||
// stopped when the component unmounts
|
|
||||||
export function recordInstanceBoundEffect(
|
|
||||||
effect: ReactiveEffect,
|
|
||||||
instance = currentInstance
|
|
||||||
) {
|
|
||||||
if (instance) {
|
|
||||||
;(instance.effects || (instance.effects = [])).push(effect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const classifyRE = /(?:^|[-_])(\w)/g
|
const classifyRE = /(?:^|[-_])(\w)/g
|
||||||
const classify = (str: string): string =>
|
const classify = (str: string): string =>
|
||||||
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
|
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
NOOP,
|
NOOP,
|
||||||
isPromise
|
isPromise
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { computed } from './apiComputed'
|
import { computed } from '@vue/reactivity'
|
||||||
import {
|
import {
|
||||||
watch,
|
watch,
|
||||||
WatchOptions,
|
WatchOptions,
|
||||||
|
@ -29,7 +29,8 @@ import {
|
|||||||
ComponentInternalInstance,
|
ComponentInternalInstance,
|
||||||
ComponentOptions,
|
ComponentOptions,
|
||||||
ConcreteComponent,
|
ConcreteComponent,
|
||||||
setCurrentInstance
|
setCurrentInstance,
|
||||||
|
unsetCurrentInstance
|
||||||
} from './component'
|
} from './component'
|
||||||
import { isEmitListener } from './componentEmits'
|
import { isEmitListener } from './componentEmits'
|
||||||
import { InternalObjectKey } from './vnode'
|
import { InternalObjectKey } from './vnode'
|
||||||
@ -411,7 +412,7 @@ function resolvePropValue(
|
|||||||
: null,
|
: null,
|
||||||
props
|
props
|
||||||
)
|
)
|
||||||
setCurrentInstance(null)
|
unsetCurrentInstance()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = defaultValue
|
value = defaultValue
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
export const version = __VERSION__
|
export const version = __VERSION__
|
||||||
export {
|
export {
|
||||||
// core
|
// core
|
||||||
|
computed,
|
||||||
reactive,
|
reactive,
|
||||||
ref,
|
ref,
|
||||||
readonly,
|
readonly,
|
||||||
@ -22,9 +23,17 @@ export {
|
|||||||
shallowReactive,
|
shallowReactive,
|
||||||
shallowReadonly,
|
shallowReadonly,
|
||||||
markRaw,
|
markRaw,
|
||||||
toRaw
|
toRaw,
|
||||||
|
// effect
|
||||||
|
effect,
|
||||||
|
stop,
|
||||||
|
ReactiveEffect,
|
||||||
|
// effect scope
|
||||||
|
effectScope,
|
||||||
|
EffectScope,
|
||||||
|
getCurrentScope,
|
||||||
|
onScopeDispose
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
export { computed } from './apiComputed'
|
|
||||||
export { watch, watchEffect } from './apiWatch'
|
export { watch, watchEffect } from './apiWatch'
|
||||||
export {
|
export {
|
||||||
onBeforeMount,
|
onBeforeMount,
|
||||||
@ -137,7 +146,6 @@ declare module '@vue/reactivity' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ReactiveEffect,
|
|
||||||
ReactiveEffectOptions,
|
ReactiveEffectOptions,
|
||||||
DebuggerEvent,
|
DebuggerEvent,
|
||||||
TrackOpTypes,
|
TrackOpTypes,
|
||||||
|
@ -1622,11 +1622,12 @@ function baseCreateRenderer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create reactive effect for rendering
|
// create reactive effect for rendering
|
||||||
const effect = (instance.effect = new ReactiveEffect(
|
const effect = new ReactiveEffect(
|
||||||
componentUpdateFn,
|
componentUpdateFn,
|
||||||
() => queueJob(instance.update),
|
() => queueJob(instance.update),
|
||||||
|
instance.scope, // track it in component's effect scope
|
||||||
true /* allowRecurse */
|
true /* allowRecurse */
|
||||||
))
|
)
|
||||||
|
|
||||||
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
|
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
|
||||||
update.id = instance.uid
|
update.id = instance.uid
|
||||||
@ -2285,12 +2286,13 @@ function baseCreateRenderer(
|
|||||||
unregisterHMR(instance)
|
unregisterHMR(instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { bum, effect, effects, update, subTree, um } = instance
|
const { bum, scope, update, subTree, um } = instance
|
||||||
|
|
||||||
// beforeUnmount hook
|
// beforeUnmount hook
|
||||||
if (bum) {
|
if (bum) {
|
||||||
invokeArrayFns(bum)
|
invokeArrayFns(bum)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
__COMPAT__ &&
|
__COMPAT__ &&
|
||||||
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
|
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
|
||||||
@ -2298,15 +2300,13 @@ function baseCreateRenderer(
|
|||||||
instance.emit('hook:beforeDestroy')
|
instance.emit('hook:beforeDestroy')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effects) {
|
if (scope) {
|
||||||
for (let i = 0; i < effects.length; i++) {
|
scope.stop()
|
||||||
effects[i].stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update may be null if a component is unmounted before its async
|
// update may be null if a component is unmounted before its async
|
||||||
// setup has resolved.
|
// setup has resolved.
|
||||||
if (effect) {
|
if (update) {
|
||||||
effect.stop()
|
|
||||||
// so that scheduler will no longer invoke it
|
// so that scheduler will no longer invoke it
|
||||||
update.active = false
|
update.active = false
|
||||||
unmount(subTree, instance, parentSuspense, doRemove)
|
unmount(subTree, instance, parentSuspense, doRemove)
|
||||||
|
Loading…
Reference in New Issue
Block a user