INSTANCE_DELETE,
INSTANCE_MOUNT,
INSTANCE_DESTROY,
+ INSTANCE_EVENT_EMITTER,
+ INSTANCE_EVENT_HOOKS,
OPTIONS_DATA_FN,
OPTIONS_DATA_MERGE,
link: `https://v3.vuejs.org/api/application-api.html#unmount`
},
+ [DeprecationTypes.INSTANCE_EVENT_EMITTER]: {
+ message:
+ `vm.$on/$once/$off() have been removed. ` +
+ `Use an external event emitter library instead.`,
+ link: `https://v3.vuejs.org/guide/migration/events-api.html`
+ },
+
+ [DeprecationTypes.INSTANCE_EVENT_HOOKS]: {
+ message:
+ `"hook:x" lifecycle events are no longer supported. ` +
+ `Use Composition API to dynamically register lifecycle hooks.`,
+ link: `https://v3.vuejs.org/api/composition-api.html#lifecycle-hooks`
+ },
+
[DeprecationTypes.OPTIONS_DATA_FN]: {
message:
`The "data" option can no longer be a plain object. ` +
}
}
+const hasWarned: Record<string, boolean> = {}
+
export function warnDeprecation(key: DeprecationTypes, ...args: any[]) {
if (!__COMPAT__ || !__DEV__) {
return
}
+ const dupKey = key + args.join('')
+ if (hasWarned[dupKey]) {
+ return
+ }
+ hasWarned[dupKey] = true
const { message, link } = deprecations[key]
warn(
`[DEPRECATION] ${
--- /dev/null
+import { isArray } from '@vue/shared'
+import { ComponentInternalInstance } from '../component'
+import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling'
+import { DeprecationTypes, warnDeprecation } from './deprecations'
+
+interface EventRegistry {
+ [event: string]: Function[] | undefined
+}
+
+const eventRegistryMap = /*#__PURE__*/ new WeakMap<
+ ComponentInternalInstance,
+ EventRegistry
+>()
+
+export function getRegistry(
+ instance: ComponentInternalInstance
+): EventRegistry {
+ let events = eventRegistryMap.get(instance)
+ if (!events) {
+ eventRegistryMap.set(instance, (events = Object.create(null)))
+ }
+ return events!
+}
+
+export function on(
+ instance: ComponentInternalInstance,
+ event: string | string[],
+ fn: Function
+) {
+ if (isArray(event)) {
+ event.forEach(e => on(instance, e, fn))
+ } else {
+ const events = getRegistry(instance)
+ ;(events[event] || (events[event] = [])).push(fn)
+ if (__DEV__) {
+ if (event.startsWith('hook:')) {
+ warnDeprecation(DeprecationTypes.INSTANCE_EVENT_HOOKS)
+ } else {
+ warnDeprecation(DeprecationTypes.INSTANCE_EVENT_EMITTER)
+ }
+ }
+ }
+ return instance.proxy
+}
+
+export function once(
+ instance: ComponentInternalInstance,
+ event: string,
+ fn: Function
+) {
+ const wrapped = (...args: any[]) => {
+ off(instance, event, wrapped)
+ fn.call(instance.proxy, ...args)
+ }
+ wrapped.fn = fn
+ on(instance, event, wrapped)
+ return instance.proxy
+}
+
+export function off(
+ instance: ComponentInternalInstance,
+ event?: string,
+ fn?: Function
+) {
+ __DEV__ && warnDeprecation(DeprecationTypes.INSTANCE_EVENT_EMITTER)
+ const vm = instance.proxy
+ // all
+ if (!arguments.length) {
+ eventRegistryMap.set(instance, Object.create(null))
+ return vm
+ }
+ // array of events
+ if (isArray(event)) {
+ event.forEach(e => off(instance, e, fn))
+ return vm
+ }
+ // specific event
+ const events = getRegistry(instance)
+ const cbs = events[event!]
+ if (!cbs) {
+ return vm
+ }
+ if (!fn) {
+ events[event!] = undefined
+ return vm
+ }
+ events[event!] = cbs.filter(cb => !(cb === fn || (cb as any).fn === fn))
+ return vm
+}
+
+export function emit(
+ instance: ComponentInternalInstance,
+ event: string,
+ ...args: any[]
+) {
+ const cbs = getRegistry(instance)[event]
+ if (cbs) {
+ callWithAsyncErrorHandling(
+ cbs,
+ instance,
+ ErrorCodes.COMPONENT_EVENT_HANDLER,
+ args
+ )
+ }
+ return instance.proxy
+}
import { extend, NOOP } from '@vue/shared'
import { PublicPropertiesMap } from '../componentPublicInstance'
import { DeprecationTypes, warnDeprecation } from './deprecations'
+import { off, on, once } from './eventEmitter'
export function installCompatInstanceProperties(map: PublicPropertiesMap) {
const set = (target: any, key: any, val: any) => {
__DEV__ && warnDeprecation(DeprecationTypes.INSTANCE_DESTROY)
// root destroy override from ./global.ts in installCompatMount
return i.ctx._compat_destroy || NOOP
- }
+ },
+ $on: i => on.bind(null, i),
+ $once: i => once.bind(null, i),
+ $off: i => off.bind(null, i)
} as PublicPropertiesMap)
}
import { UnionToIntersection } from './helpers/typeUtils'
import { devtoolsComponentEmit } from './devtools'
import { AppContext } from './apiCreateApp'
+import { emit as compatEmit } from './compat/eventEmitter'
export type ObjectEmitsOptions = Record<
string,
args
)
}
+
+ if (__COMPAT__) {
+ return compatEmit(instance, event, args)
+ }
}
export function normalizeEmitsOptions(
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parent, initialVNode)
}
+ if (__COMPAT__) {
+ instance.emit('hook:beforeMount')
+ }
// render
if (__DEV__) {
// onVnodeMounted
if ((vnodeHook = props && props.onVnodeMounted)) {
const scopedInitialVNode = initialVNode
- queuePostRenderEffect(() => {
- invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode)
- }, parentSuspense)
+ queuePostRenderEffect(
+ () => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
+ parentSuspense
+ )
}
+ if (__COMPAT__) {
+ queuePostRenderEffect(
+ () => instance.emit('hook:mounted'),
+ parentSuspense
+ )
+ }
+
// activated hook for keep-alive roots.
// #1742 activated hook must be accessed after first render
// since the hook may be injected by a child keep-alive
- const { a } = instance
- if (
- a &&
- initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
- ) {
- queuePostRenderEffect(a, parentSuspense)
+ if (initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
+ instance.a && queuePostRenderEffect(instance.a, parentSuspense)
+ if (__COMPAT__) {
+ queuePostRenderEffect(
+ () => instance.emit('hook:activated'),
+ parentSuspense
+ )
+ }
}
instance.isMounted = true
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parent, next, vnode)
}
+ if (__COMPAT__) {
+ instance.emit('hook:beforeUpdate')
+ }
// render
if (__DEV__) {
}
// onVnodeUpdated
if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
- queuePostRenderEffect(() => {
- invokeVNodeHook(vnodeHook!, parent, next!, vnode)
- }, parentSuspense)
+ queuePostRenderEffect(
+ () => invokeVNodeHook(vnodeHook!, parent, next!, vnode),
+ parentSuspense
+ )
+ }
+ if (__COMPAT__) {
+ queuePostRenderEffect(
+ () => instance.emit('hook:updated'),
+ parentSuspense
+ )
}
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
}
const { bum, effects, update, subTree, um } = instance
+
// beforeUnmount hook
if (bum) {
invokeArrayFns(bum)
}
+ if (__COMPAT__) {
+ instance.emit('hook:beforeDestroy')
+ }
+
if (effects) {
for (let i = 0; i < effects.length; i++) {
stop(effects[i])
if (um) {
queuePostRenderEffect(um, parentSuspense)
}
+ if (__COMPAT__) {
+ queuePostRenderEffect(
+ () => instance.emit('hook:destroyed'),
+ parentSuspense
+ )
+ }
queuePostRenderEffect(() => {
instance.isUnmounted = true
}, parentSuspense)