mirror of
https://gitee.com/vuejs/vue.git
synced 2024-11-30 02:57:43 +08:00
wip: effectScope
This commit is contained in:
parent
b4c511da8f
commit
9fb4f7d070
@ -56,7 +56,7 @@ export function updateComponentListeners(
|
||||
target = undefined
|
||||
}
|
||||
|
||||
export function eventsMixin(Vue: Component) {
|
||||
export function eventsMixin(Vue: typeof Component) {
|
||||
const hookRE = /^hook:/
|
||||
Vue.prototype.$on = function (
|
||||
event: string | Array<string>,
|
||||
|
@ -9,10 +9,11 @@ import { initProvide, initInjections } from './inject'
|
||||
import { extend, mergeOptions, formatComponentName } from '../util/index'
|
||||
import type { Component } from 'typescript/component'
|
||||
import type { InternalComponentOptions } from 'typescript/options'
|
||||
import { EffectScope } from 'v3'
|
||||
|
||||
let uid = 0
|
||||
|
||||
export function initMixin(Vue: Component) {
|
||||
export function initMixin(Vue: typeof Component) {
|
||||
Vue.prototype._init = function (options?: Record<string, any>) {
|
||||
const vm: Component = this
|
||||
// a uid
|
||||
@ -31,6 +32,8 @@ export function initMixin(Vue: Component) {
|
||||
vm._isVue = true
|
||||
// avoid instances from being observed
|
||||
vm.__v_skip = true
|
||||
// effect scope
|
||||
vm._scope = new EffectScope(true /* detached */)
|
||||
// merge options
|
||||
if (options && options._isComponent) {
|
||||
// optimize internal component instantiation
|
||||
@ -96,7 +99,7 @@ export function initInternalComponent(
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveConstructorOptions(Ctor: Component) {
|
||||
export function resolveConstructorOptions(Ctor: typeof Component) {
|
||||
let options = Ctor.options
|
||||
if (Ctor.super) {
|
||||
const superOptions = resolveConstructorOptions(Ctor.super)
|
||||
@ -120,7 +123,9 @@ export function resolveConstructorOptions(Ctor: Component) {
|
||||
return options
|
||||
}
|
||||
|
||||
function resolveModifiedOptions(Ctor: Component): Record<string, any> | null {
|
||||
function resolveModifiedOptions(
|
||||
Ctor: typeof Component
|
||||
): Record<string, any> | null {
|
||||
let modified
|
||||
const latest = Ctor.options
|
||||
const sealed = Ctor.sealedOptions
|
||||
|
@ -57,7 +57,7 @@ export function initLifecycle(vm: Component) {
|
||||
vm._isBeingDestroyed = false
|
||||
}
|
||||
|
||||
export function lifecycleMixin(Vue: Component) {
|
||||
export function lifecycleMixin(Vue: typeof Component) {
|
||||
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
|
||||
const vm: Component = this
|
||||
const prevEl = vm.$el
|
||||
@ -108,14 +108,9 @@ export function lifecycleMixin(Vue: Component) {
|
||||
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
|
||||
remove(parent.$children, vm)
|
||||
}
|
||||
// teardown watchers
|
||||
if (vm._watcher) {
|
||||
vm._watcher.teardown()
|
||||
}
|
||||
let i = vm._watchers.length
|
||||
while (i--) {
|
||||
vm._watchers[i].teardown()
|
||||
}
|
||||
// teardown scope. this includes both the render watcher and other
|
||||
// watchers created
|
||||
vm._scope.stop()
|
||||
// remove reference from data ob
|
||||
// frozen object may not have observer.
|
||||
if (vm._data.__ob__) {
|
||||
|
@ -86,11 +86,11 @@ export function setCurrentRenderingInstance(vm: Component) {
|
||||
currentRenderingInstance = vm
|
||||
}
|
||||
|
||||
export function renderMixin(Vue: Component) {
|
||||
export function renderMixin(Vue: typeof Component) {
|
||||
// install runtime convenience helpers
|
||||
installRenderHelpers(Vue.prototype)
|
||||
|
||||
Vue.prototype.$nextTick = function (fn: Function) {
|
||||
Vue.prototype.$nextTick = function (fn: (...args: any[]) => any) {
|
||||
return nextTick(fn, this)
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ import {
|
||||
invokeWithErrorHandling,
|
||||
isFunction
|
||||
} from '../util/index'
|
||||
import type { Component } from '../../../typescript/component'
|
||||
import type { Component } from 'typescript/component'
|
||||
|
||||
const sharedPropertyDefinition = {
|
||||
enumerable: true,
|
||||
@ -49,7 +49,6 @@ export function proxy(target: Object, sourceKey: string, key: string) {
|
||||
}
|
||||
|
||||
export function initState(vm: Component) {
|
||||
vm._watchers = []
|
||||
const opts = vm.$options
|
||||
if (opts.props) initProps(vm, opts.props)
|
||||
|
||||
@ -310,7 +309,7 @@ function initWatch(vm: Component, watch: Object) {
|
||||
|
||||
function createWatcher(
|
||||
vm: Component,
|
||||
expOrFn: string | Function,
|
||||
expOrFn: string | (() => any),
|
||||
handler: any,
|
||||
options?: Object
|
||||
) {
|
||||
@ -324,7 +323,7 @@ function createWatcher(
|
||||
return vm.$watch(expOrFn, handler, options)
|
||||
}
|
||||
|
||||
export function stateMixin(Vue: Component) {
|
||||
export function stateMixin(Vue: typeof Component) {
|
||||
// flow somehow has problems with directly declared definition object
|
||||
// when using Object.defineProperty, so we have to procedurally build up
|
||||
// the object here.
|
||||
|
@ -16,6 +16,10 @@ import Dep, { pushTarget, popTarget, DepTarget } from './dep'
|
||||
|
||||
import type { SimpleSet } from '../util/index'
|
||||
import type { Component } from 'typescript/component'
|
||||
import {
|
||||
activeEffectScope,
|
||||
recordEffectScope
|
||||
} from '../../v3/reactivity/effectScope'
|
||||
|
||||
let uid = 0
|
||||
|
||||
@ -59,11 +63,11 @@ export default class Watcher implements DepTarget {
|
||||
} | null,
|
||||
isRenderWatcher?: boolean
|
||||
) {
|
||||
recordEffectScope(this, activeEffectScope || (vm ? vm._scope : undefined))
|
||||
if ((this.vm = vm)) {
|
||||
if (isRenderWatcher) {
|
||||
vm._watcher = this
|
||||
}
|
||||
vm._watchers.push(this)
|
||||
}
|
||||
// options
|
||||
if (options) {
|
||||
@ -237,13 +241,10 @@ export default class Watcher implements DepTarget {
|
||||
* Remove self from all dependencies' subscriber list.
|
||||
*/
|
||||
teardown() {
|
||||
if (this.vm && !this.vm._isBeingDestroyed) {
|
||||
remove(this.vm._scope.effects, this)
|
||||
}
|
||||
if (this.active) {
|
||||
// remove self from vm's watcher list
|
||||
// this is a somewhat expensive operation so we skip it
|
||||
// if the vm is being destroyed.
|
||||
if (this.vm && !this.vm._isBeingDestroyed) {
|
||||
remove(this.vm._watchers, this)
|
||||
}
|
||||
let i = this.deps.length
|
||||
while (i--) {
|
||||
this.deps[i].removeSub(this)
|
||||
|
@ -85,7 +85,9 @@ if (typeof Promise !== 'undefined' && isNative(Promise)) {
|
||||
}
|
||||
}
|
||||
|
||||
export function nextTick(cb?: Function, ctx?: Object) {
|
||||
export function nextTick(): Promise<void>
|
||||
export function nextTick(cb: (...args: any[]) => any, ctx?: object): void
|
||||
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
|
||||
let _resolve
|
||||
callbacks.push(() => {
|
||||
if (cb) {
|
||||
|
@ -26,7 +26,10 @@ import type {
|
||||
VNodeWithData
|
||||
} from 'typescript/vnode'
|
||||
import type { Component } from 'typescript/component'
|
||||
import type { InternalComponentOptions } from 'typescript/options'
|
||||
import type {
|
||||
ComponentOptions,
|
||||
InternalComponentOptions
|
||||
} from 'typescript/options'
|
||||
|
||||
// inline hooks to be invoked on component VNodes during patch
|
||||
const componentVNodeHooks = {
|
||||
@ -95,7 +98,7 @@ const componentVNodeHooks = {
|
||||
const hooksToMerge = Object.keys(componentVNodeHooks)
|
||||
|
||||
export function createComponent(
|
||||
Ctor: Component | Function | Object | void,
|
||||
Ctor: typeof Component | Function | ComponentOptions | void,
|
||||
data: VNodeData | undefined,
|
||||
context: Component,
|
||||
children?: Array<VNode>,
|
||||
@ -109,7 +112,7 @@ export function createComponent(
|
||||
|
||||
// plain options object: turn it into a constructor
|
||||
if (isObject(Ctor)) {
|
||||
Ctor = baseCtor.extend(Ctor)
|
||||
Ctor = baseCtor.extend(Ctor as typeof Component)
|
||||
}
|
||||
|
||||
// if at this stage it's not a constructor or an async component factory,
|
||||
@ -139,7 +142,7 @@ export function createComponent(
|
||||
|
||||
// resolve constructor options in case global mixins are applied after
|
||||
// component constructor creation
|
||||
resolveConstructorOptions(Ctor as Component)
|
||||
resolveConstructorOptions(Ctor as typeof Component)
|
||||
|
||||
// transform component v-model data into props & events
|
||||
if (isDef(data.model)) {
|
||||
@ -155,7 +158,7 @@ export function createComponent(
|
||||
// @ts-expect-error
|
||||
if (isTrue(Ctor.options.functional)) {
|
||||
return createFunctionalComponent(
|
||||
Ctor as Component,
|
||||
Ctor as typeof Component,
|
||||
propsData,
|
||||
data,
|
||||
context,
|
||||
|
@ -126,7 +126,7 @@ export function _createElement(
|
||||
}
|
||||
} else {
|
||||
// direct component options / constructor
|
||||
vnode = createComponent(tag, data, context, children)
|
||||
vnode = createComponent(tag as any, data, context, children)
|
||||
}
|
||||
if (isArray(vnode)) {
|
||||
return vnode
|
||||
|
@ -23,7 +23,7 @@ export function FunctionalRenderContext(
|
||||
props: Object,
|
||||
children: Array<VNode> | undefined,
|
||||
parent: Component,
|
||||
Ctor: Component
|
||||
Ctor: typeof Component
|
||||
) {
|
||||
const options = Ctor.options
|
||||
// ensure the createElement function in functional components
|
||||
@ -31,14 +31,13 @@ export function FunctionalRenderContext(
|
||||
let contextVm
|
||||
if (hasOwn(parent, '_uid')) {
|
||||
contextVm = Object.create(parent)
|
||||
// $flow-disable-line
|
||||
contextVm._original = parent
|
||||
} else {
|
||||
// the context vm passed in is a functional context as well.
|
||||
// in this case we want to make sure we are able to get a hold to the
|
||||
// real context instance.
|
||||
contextVm = parent
|
||||
// $flow-disable-line
|
||||
// @ts-ignore
|
||||
parent = parent._original
|
||||
}
|
||||
const isCompiled = isTrue(options._compiled)
|
||||
@ -94,7 +93,7 @@ export function FunctionalRenderContext(
|
||||
installRenderHelpers(FunctionalRenderContext.prototype)
|
||||
|
||||
export function createFunctionalComponent(
|
||||
Ctor: Component,
|
||||
Ctor: typeof Component,
|
||||
propsData: Object | undefined,
|
||||
data: VNodeData,
|
||||
contextVm: Component,
|
||||
|
@ -11,7 +11,7 @@ import type { VNodeData } from 'typescript/vnode'
|
||||
|
||||
export function extractPropsFromVNodeData(
|
||||
data: VNodeData,
|
||||
Ctor: Component,
|
||||
Ctor: typeof Component,
|
||||
tag?: string
|
||||
): object | undefined {
|
||||
// we are only extracting raw values here.
|
||||
|
@ -37,8 +37,8 @@ export function createAsyncPlaceholder(
|
||||
|
||||
export function resolveAsyncComponent(
|
||||
factory: { (...args: any[]): any; [keye: string]: any },
|
||||
baseCtor: Component
|
||||
): Component | void {
|
||||
baseCtor: typeof Component
|
||||
): typeof Component | void {
|
||||
if (isTrue(factory.error) && isDef(factory.errorComp)) {
|
||||
return factory.errorComp
|
||||
}
|
||||
|
@ -10,15 +10,7 @@ export function initSetup(vm: Component) {
|
||||
const options = vm.$options
|
||||
const setup = options.setup
|
||||
if (setup) {
|
||||
const ctx = {
|
||||
get attrs() {
|
||||
return initAttrsProxy(vm)
|
||||
},
|
||||
get slots() {
|
||||
return initSlotsProxy(vm)
|
||||
},
|
||||
emit: bind(vm.$emit, vm) as any
|
||||
}
|
||||
const ctx = (vm._setupContext = createSetupContext(vm))
|
||||
|
||||
setCurrentInstance(vm)
|
||||
const setupResult = invokeWithErrorHandling(
|
||||
@ -73,6 +65,18 @@ function proxySetupProperty(
|
||||
})
|
||||
}
|
||||
|
||||
function createSetupContext(vm: Component) {
|
||||
return {
|
||||
get attrs() {
|
||||
return initAttrsProxy(vm)
|
||||
},
|
||||
get slots() {
|
||||
return initSlotsProxy(vm)
|
||||
},
|
||||
emit: bind(vm.$emit, vm) as any
|
||||
}
|
||||
}
|
||||
|
||||
function initAttrsProxy(vm: Component) {
|
||||
if (!vm._attrsProxy) {
|
||||
const proxy = (vm._attrsProxy = {})
|
||||
@ -146,5 +150,6 @@ function getContext(): SetupContext {
|
||||
if (__DEV__ && !currentInstance) {
|
||||
warn(`useContext() called without active instance.`)
|
||||
}
|
||||
return currentInstance!.setupContext
|
||||
const vm = currentInstance!
|
||||
return vm._setupContext || (vm._setupContext = createSetupContext(vm))
|
||||
}
|
||||
|
@ -245,7 +245,7 @@ function doWatch(
|
||||
} else {
|
||||
// no cb -> simple effect
|
||||
getter = () => {
|
||||
if (instance && instance.isUnmounted) {
|
||||
if (instance && instance._isDestroyed) {
|
||||
return
|
||||
}
|
||||
if (cleanup) {
|
||||
|
@ -17,5 +17,7 @@ export function getCurrentInstance(): { proxy: Component } | null {
|
||||
* @private
|
||||
*/
|
||||
export function setCurrentInstance(vm: Component | null = null) {
|
||||
if (!vm) currentInstance && currentInstance._scope.off()
|
||||
currentInstance = vm
|
||||
vm && vm._scope.on()
|
||||
}
|
||||
|
@ -57,10 +57,18 @@ export {
|
||||
DebuggerEvent
|
||||
} from './apiWatch'
|
||||
|
||||
export {
|
||||
EffectScope,
|
||||
effectScope,
|
||||
onScopeDispose,
|
||||
getCurrentScope
|
||||
} from './reactivity/effectScope'
|
||||
|
||||
export { TrackOpTypes, TriggerOpTypes } from './reactivity/operations'
|
||||
|
||||
export { h } from './h'
|
||||
export { getCurrentInstance } from './currentInstance'
|
||||
export { useSlots, useAttrs } from './apiSetup'
|
||||
export { nextTick } from 'core/util'
|
||||
|
||||
export * from './apiLifecycle'
|
||||
|
131
src/v3/reactivity/effectScope.ts
Normal file
131
src/v3/reactivity/effectScope.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import Watcher from 'core/observer/watcher'
|
||||
import { warn } from 'core/util'
|
||||
|
||||
export let activeEffectScope: EffectScope | undefined
|
||||
|
||||
export class EffectScope {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
active = true
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
effects: Watcher[] = []
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
cleanups: (() => void)[] = []
|
||||
|
||||
/**
|
||||
* only assigned by undetached scope
|
||||
* @internal
|
||||
*/
|
||||
parent: EffectScope | undefined
|
||||
/**
|
||||
* record undetached scopes
|
||||
* @internal
|
||||
*/
|
||||
scopes: EffectScope[] | undefined
|
||||
/**
|
||||
* track a child scope's index in its parent's scopes array for optimized
|
||||
* removal
|
||||
* @internal
|
||||
*/
|
||||
private index: number | undefined
|
||||
|
||||
constructor(detached = false) {
|
||||
if (!detached && activeEffectScope) {
|
||||
this.parent = activeEffectScope
|
||||
this.index =
|
||||
(activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
|
||||
this
|
||||
) - 1
|
||||
}
|
||||
}
|
||||
|
||||
run<T>(fn: () => T): T | undefined {
|
||||
if (this.active) {
|
||||
const currentEffectScope = activeEffectScope
|
||||
try {
|
||||
activeEffectScope = this
|
||||
return fn()
|
||||
} finally {
|
||||
activeEffectScope = currentEffectScope
|
||||
}
|
||||
} else if (__DEV__) {
|
||||
warn(`cannot run an inactive effect scope.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This should only be called on non-detached scopes
|
||||
* @internal
|
||||
*/
|
||||
on() {
|
||||
activeEffectScope = this
|
||||
}
|
||||
|
||||
/**
|
||||
* This should only be called on non-detached scopes
|
||||
* @internal
|
||||
*/
|
||||
off() {
|
||||
activeEffectScope = this.parent
|
||||
}
|
||||
|
||||
stop(fromParent?: boolean) {
|
||||
if (this.active) {
|
||||
let i, l
|
||||
for (i = 0, l = this.effects.length; i < l; i++) {
|
||||
this.effects[i].teardown()
|
||||
}
|
||||
for (i = 0, l = this.cleanups.length; i < l; i++) {
|
||||
this.cleanups[i]()
|
||||
}
|
||||
if (this.scopes) {
|
||||
for (i = 0, l = this.scopes.length; i < l; i++) {
|
||||
this.scopes[i].stop(true)
|
||||
}
|
||||
}
|
||||
// nested scope, dereference from parent to avoid memory leaks
|
||||
if (this.parent && !fromParent) {
|
||||
// optimized O(1) removal
|
||||
const last = this.parent.scopes!.pop()
|
||||
if (last && last !== this) {
|
||||
this.parent.scopes![this.index!] = last
|
||||
last.index = this.index!
|
||||
}
|
||||
}
|
||||
this.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function effectScope(detached?: boolean) {
|
||||
return new EffectScope(detached)
|
||||
}
|
||||
|
||||
export function recordEffectScope(
|
||||
effect: Watcher,
|
||||
scope: EffectScope | undefined = 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(
|
||||
`onScopeDispose() is called when there is no active effect scope` +
|
||||
` to be associated with.`
|
||||
)
|
||||
}
|
||||
}
|
@ -107,7 +107,7 @@ describe('Instance methods lifecycle', () => {
|
||||
vm.$watch('a', () => {})
|
||||
vm.$destroy()
|
||||
expect(vm._watcher.active).toBe(false)
|
||||
expect(vm._watchers.every(w => !w.active)).toBe(true)
|
||||
expect(vm._scope.effects.every(w => !w.active)).toBe(true)
|
||||
})
|
||||
|
||||
it('remove self from data observer', () => {
|
||||
|
@ -10,10 +10,13 @@ import {
|
||||
triggerRef,
|
||||
shallowRef,
|
||||
h,
|
||||
onMounted
|
||||
onMounted,
|
||||
getCurrentInstance,
|
||||
effectScope
|
||||
} from 'v3'
|
||||
import { nextTick } from 'core/util'
|
||||
import { set } from 'core/observer'
|
||||
import { Component } from 'typescript/component'
|
||||
|
||||
// reference: https://vue-composition-api-rfc.netlify.com/api.html#watch
|
||||
|
||||
@ -1008,35 +1011,6 @@ describe('api: watch', () => {
|
||||
expect(cb).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('watching keypath', async () => {
|
||||
const spy = vi.fn()
|
||||
const Comp = {
|
||||
render() {},
|
||||
data() {
|
||||
return {
|
||||
a: {
|
||||
b: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'a.b': spy
|
||||
},
|
||||
created(this: any) {
|
||||
this.$watch('a.b', spy)
|
||||
},
|
||||
mounted(this: any) {
|
||||
this.a.b++
|
||||
}
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
new Vue(Comp).$mount(root)
|
||||
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('watching sources: ref<any[]>', async () => {
|
||||
const foo = ref([1])
|
||||
const spy = vi.fn()
|
||||
@ -1062,25 +1036,24 @@ describe('api: watch', () => {
|
||||
})
|
||||
|
||||
// vuejs/core#4158
|
||||
// TODO
|
||||
// test.skip('watch should not register in owner component if created inside detached scope', () => {
|
||||
// let instance: Component
|
||||
// const Comp = {
|
||||
// setup() {
|
||||
// instance = getCurrentInstance()!.proxy
|
||||
// effectScope(true).run(() => {
|
||||
// watch(
|
||||
// () => 1,
|
||||
// () => {}
|
||||
// )
|
||||
// })
|
||||
// return () => ''
|
||||
// }
|
||||
// }
|
||||
// const root = document.createElement('div')
|
||||
// new Vue(Comp).$mount(root)
|
||||
// // should not record watcher in detached scope and only the instance's
|
||||
// // own update effect
|
||||
// expect(instance!.scope.effects.length).toBe(1)
|
||||
// })
|
||||
test('watch should not register in owner component if created inside detached scope', () => {
|
||||
let instance: Component
|
||||
const Comp = {
|
||||
setup() {
|
||||
instance = getCurrentInstance()!.proxy
|
||||
effectScope(true).run(() => {
|
||||
watch(
|
||||
() => 1,
|
||||
() => {}
|
||||
)
|
||||
})
|
||||
return () => ''
|
||||
}
|
||||
}
|
||||
const root = document.createElement('div')
|
||||
new Vue(Comp).$mount(root)
|
||||
// should not record watcher in detached scope and only the instance's
|
||||
// own update effect
|
||||
expect(instance!._scope.effects.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
282
test/unit/features/v3/reactivity/effectScope.spec.ts
Normal file
282
test/unit/features/v3/reactivity/effectScope.spec.ts
Normal file
@ -0,0 +1,282 @@
|
||||
import { nextTick } from 'core/util'
|
||||
import {
|
||||
watch,
|
||||
watchEffect,
|
||||
reactive,
|
||||
computed,
|
||||
ref,
|
||||
ComputedRef,
|
||||
EffectScope,
|
||||
onScopeDispose,
|
||||
getCurrentScope
|
||||
} from 'v3/index'
|
||||
import { effect } from 'v3/reactivity/effect'
|
||||
|
||||
describe('reactivity/effectScope', () => {
|
||||
it('should run', () => {
|
||||
const fnSpy = vi.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(1)
|
||||
expect(scope.scopes!.length).toBe(1)
|
||||
expect(scope.scopes![0]).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 stopped
|
||||
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('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 onScopeDispose 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('should warn onScopeDispose() is called when there is no active effect scope', () => {
|
||||
const spy = vi.fn()
|
||||
const scope = new EffectScope()
|
||||
scope.run(() => {
|
||||
onScopeDispose(spy)
|
||||
})
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(0)
|
||||
|
||||
onScopeDispose(spy)
|
||||
|
||||
expect(
|
||||
'onScopeDispose() is called when there is no active effect scope to be associated with.'
|
||||
).toHaveBeenWarned()
|
||||
|
||||
scope.stop()
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should dereference child scope from parent scope after stopping child scope (no memleaks)', () => {
|
||||
const parent = new EffectScope()
|
||||
const child = parent.run(() => new EffectScope())!
|
||||
expect(parent.scopes!.includes(child)).toBe(true)
|
||||
child.stop()
|
||||
expect(parent.scopes!.includes(child)).toBe(false)
|
||||
})
|
||||
|
||||
it('test with higher level APIs', async () => {
|
||||
const r = ref(1)
|
||||
|
||||
const computedSpy = vi.fn()
|
||||
const watchSpy = vi.fn()
|
||||
const watchEffectSpy = vi.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)
|
||||
})
|
||||
|
||||
it('getCurrentScope() stays valid when running a detached nested EffectScope', () => {
|
||||
const parentScope = new EffectScope()
|
||||
|
||||
parentScope.run(() => {
|
||||
const currentScope = getCurrentScope()
|
||||
expect(currentScope).toBeDefined()
|
||||
const detachedScope = new EffectScope(true)
|
||||
detachedScope.run(() => {})
|
||||
|
||||
expect(getCurrentScope()).toBe(currentScope)
|
||||
})
|
||||
})
|
||||
})
|
17
typescript/component.d.ts
vendored
17
typescript/component.d.ts
vendored
@ -1,8 +1,9 @@
|
||||
import type VNode from '../src/core/vdom/vnode'
|
||||
import type Watcher from '../src/core/observer/watcher'
|
||||
import { ComponentOptions } from './options'
|
||||
import { ComponentOptions, SetupContext } from './options'
|
||||
import { ScopedSlotsData, VNodeChildren, VNodeData } from './vnode'
|
||||
import { GlobalAPI } from './global-api'
|
||||
import { EffectScope } from 'v3'
|
||||
|
||||
// TODO this should be using the same as /component/
|
||||
|
||||
@ -16,7 +17,7 @@ export declare class Component {
|
||||
static superOptions: Record<string, any>
|
||||
static extendOptions: Record<string, any>
|
||||
static sealedOptions: Record<string, any>
|
||||
static super: Component
|
||||
static super: typeof Component
|
||||
// assets
|
||||
static directive: GlobalAPI['directive']
|
||||
static component: GlobalAPI['component']
|
||||
@ -58,7 +59,7 @@ export declare class Component {
|
||||
key: string | number
|
||||
) => void
|
||||
$watch: (
|
||||
expOrFn: string | Function,
|
||||
expOrFn: string | (() => any),
|
||||
cb: Function,
|
||||
options?: Record<string, any>
|
||||
) => Function
|
||||
@ -66,7 +67,7 @@ export declare class Component {
|
||||
$once: (event: string, fn: Function) => Component
|
||||
$off: (event?: string | Array<string>, fn?: Function) => Component
|
||||
$emit: (event: string, ...args: Array<any>) => Component
|
||||
$nextTick: (fn: Function) => void | Promise<any>
|
||||
$nextTick: (fn: (...args: any[]) => any) => void | Promise<any>
|
||||
$createElement: (
|
||||
tag?: string | Component,
|
||||
data?: Record<string, any>,
|
||||
@ -77,11 +78,12 @@ export declare class Component {
|
||||
_uid: number | string
|
||||
_name: string // this only exists in dev mode
|
||||
_isVue: true
|
||||
__v_skip: true
|
||||
_self: Component
|
||||
_renderProxy: Component
|
||||
_renderContext?: Component
|
||||
_watcher: Watcher | null
|
||||
_watchers: Array<Watcher>
|
||||
_scope: EffectScope
|
||||
_computedWatchers: { [key: string]: Watcher }
|
||||
_data: Record<string, any>
|
||||
_props: Record<string, any>
|
||||
@ -99,6 +101,7 @@ export declare class Component {
|
||||
|
||||
// @v3
|
||||
_setupState?: Record<string, any>
|
||||
_setupContext?: SetupContext
|
||||
_attrsProxy?: Record<string, any>
|
||||
_slotsProxy?: Record<string, () => VNode[]>
|
||||
_preWatchers?: Watcher[]
|
||||
@ -193,8 +196,8 @@ export declare class Component {
|
||||
_ssrAttrs: Function
|
||||
_ssrDOMProps: Function
|
||||
_ssrClass: Function
|
||||
_ssrStyle: Function;
|
||||
_ssrStyle: Function
|
||||
|
||||
// allow dynamic method registration
|
||||
[key: string]: any
|
||||
// [key: string]: any
|
||||
}
|
||||
|
3
typescript/global-api.d.ts
vendored
3
typescript/global-api.d.ts
vendored
@ -1,5 +1,6 @@
|
||||
import { Config } from '../src/core/config'
|
||||
import { Component } from './component'
|
||||
import { ComponentOptions } from './options'
|
||||
|
||||
declare interface GlobalAPI {
|
||||
// new(options?: any): Component
|
||||
@ -9,7 +10,7 @@ declare interface GlobalAPI {
|
||||
config: Config
|
||||
util: Object
|
||||
|
||||
extend: (options: Object) => typeof Component
|
||||
extend: (options: typeof Component | ComponentOptions) => typeof Component
|
||||
set: <T>(target: Object | Array<T>, key: string | number, value: T) => T
|
||||
delete: <T>(target: Object | Array<T>, key: string | number) => void
|
||||
nextTick: (fn: Function, context?: Object) => void | Promise<any>
|
||||
|
2
typescript/options.d.ts
vendored
2
typescript/options.d.ts
vendored
@ -100,7 +100,7 @@ declare type ComponentOptions = {
|
||||
_renderChildren?: Array<VNode> | null
|
||||
_componentTag: string | null
|
||||
_scopeId: string | null
|
||||
_base: Component
|
||||
_base: typeof Component
|
||||
}
|
||||
|
||||
declare type PropOptions = {
|
||||
|
2
typescript/vnode.d.ts
vendored
2
typescript/vnode.d.ts
vendored
@ -6,7 +6,7 @@ declare type VNodeChildren =
|
||||
| string
|
||||
|
||||
declare type VNodeComponentOptions = {
|
||||
Ctor: Component
|
||||
Ctor: typeof Component
|
||||
propsData?: Object
|
||||
listeners?: Record<string, Function | Function[]>
|
||||
children?: Array<VNode>
|
||||
|
Loading…
Reference in New Issue
Block a user