From: Evan You Date: Tue, 6 Apr 2021 19:58:12 +0000 (-0400) Subject: wip: instance event emitter api compat X-Git-Tag: v3.1.0-beta.1~59^2~66 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=db098056884dcd73ff837c8256da3ed4d32a3d87;p=thirdparty%2Fvuejs%2Fcore.git wip: instance event emitter api compat --- diff --git a/packages/runtime-core/src/compat/deprecations.ts b/packages/runtime-core/src/compat/deprecations.ts index 6cadda9245..f71d77ff04 100644 --- a/packages/runtime-core/src/compat/deprecations.ts +++ b/packages/runtime-core/src/compat/deprecations.ts @@ -18,6 +18,8 @@ export const enum DeprecationTypes { INSTANCE_DELETE, INSTANCE_MOUNT, INSTANCE_DESTROY, + INSTANCE_EVENT_EMITTER, + INSTANCE_EVENT_HOOKS, OPTIONS_DATA_FN, OPTIONS_DATA_MERGE, @@ -132,6 +134,20 @@ const deprecations: Record = { 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. ` + @@ -169,10 +185,17 @@ const deprecations: Record = { } } +const hasWarned: Record = {} + 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] ${ diff --git a/packages/runtime-core/src/compat/eventEmitter.ts b/packages/runtime-core/src/compat/eventEmitter.ts new file mode 100644 index 0000000000..ca89e042b4 --- /dev/null +++ b/packages/runtime-core/src/compat/eventEmitter.ts @@ -0,0 +1,106 @@ +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 +} diff --git a/packages/runtime-core/src/compat/instance.ts b/packages/runtime-core/src/compat/instance.ts index b92a1ebe10..a7158addbf 100644 --- a/packages/runtime-core/src/compat/instance.ts +++ b/packages/runtime-core/src/compat/instance.ts @@ -1,6 +1,7 @@ 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) => { @@ -29,6 +30,9 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) { __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) } diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index ab382edc80..f40e67abc6 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -21,6 +21,7 @@ import { warn } from './warning' 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, @@ -148,6 +149,10 @@ export function emit( args ) } + + if (__COMPAT__) { + return compatEmit(instance, event, args) + } } export function normalizeEmitsOptions( diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index ea065d580b..07384780e7 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1417,6 +1417,9 @@ function baseCreateRenderer( if ((vnodeHook = props && props.onVnodeBeforeMount)) { invokeVNodeHook(vnodeHook, parent, initialVNode) } + if (__COMPAT__) { + instance.emit('hook:beforeMount') + } // render if (__DEV__) { @@ -1467,19 +1470,29 @@ function baseCreateRenderer( // 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 @@ -1515,6 +1528,9 @@ function baseCreateRenderer( if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) { invokeVNodeHook(vnodeHook, parent, next, vnode) } + if (__COMPAT__) { + instance.emit('hook:beforeUpdate') + } // render if (__DEV__) { @@ -1557,9 +1573,16 @@ function baseCreateRenderer( } // 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__) { @@ -2211,10 +2234,15 @@ function baseCreateRenderer( } 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]) @@ -2230,6 +2258,12 @@ function baseCreateRenderer( if (um) { queuePostRenderEffect(um, parentSuspense) } + if (__COMPAT__) { + queuePostRenderEffect( + () => instance.emit('hook:destroyed'), + parentSuspense + ) + } queuePostRenderEffect(() => { instance.isUnmounted = true }, parentSuspense)