Preface:

Recently, I conducted project development based on VUe3.0, and made the feature of front-end burying point. The specific scheme is to monitor the events of real DOM nodes through the custom instructions of VUE. However, the internal events of the encapsulated component are triggered by $emit. We cannot capture such events in the native way. Here is my process to find a suitable solution.

$on method

Vue2.0 allows you to capture $emit emitted from the same instance using $ON method. Check github for the vue buried point scheme, which is also implemented based on this API, but unfortunately vue3.0 removes this instance method. The proposed alternatives mitt or Tiny-Emitter also don’t seem to meet our needs. Strangely enough, when I read the 3.0 source code, I could still find the $on implementation:

Path: packages\runtime-core\src\compat\instanceEventEmitter.ts 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 { if (event.startsWith('hook:')) { assertCompatEnabled( DeprecationTypes.INSTANCE_EVENT_HOOKS, instance, event ) } else { assertCompatEnabled(DeprecationTypes.INSTANCE_EVENT_EMITTER, instance) } const events = getRegistry(instance) ; (events[event] || (events[event] = [])).push(fn) } return instance.proxy } export function emit( instance: ComponentInternalInstance, event: string, args: any[] ) { const cbs = getRegistry(instance)[event] if (cbs) { callWithAsyncErrorHandling( cbs.map(cb => cb.bind(instance.proxy)), instance, ErrorCodes.COMPONENT_EVENT_HANDLER, args ) } return instance.proxy }Copy the code

In general, WeakMap stores the instance object and various events defined on the instance.$ON pushes the event into the corresponding array, and $EMIT executes the corresponding callback array. After poking around the code again, I found the __COMPAT__ variable, which seems like a way to add $on to an instance

if (__COMPAT__) {
  installCompatInstanceProperties(publicPropertiesMap)
}
export function installCompatInstanceProperties(map: PublicPropertiesMap) {
   ......
  extend(map, {
    $on: i => on.bind(null, i),
    $once: i => once.bind(null, i),
    $off: i => off.bind(null, i),
  } as PublicPropertiesMap)
   ......
}
Copy the code

But even according torollup.config.jsTo set environment variables__COMPAT__=true, still does not take effect in demo, opennode_modulesIt was found that the code for vue3.0 was already packaged, which explains why the configuration did not take effect.



At this point, there seems to be no other way to use it$on.

Vnode. Props attribute

Based on the above instruction buried point scheme, in the process of further exploring the vue source code, we found that the event mechanism used by Vue3.0 has changed. As can be seen from the following code, when $emit is executed, it will read the props attribute under the vNode of the current instance, and search and invoke according to the triggered event.

// packages\runtime-core\src\componentEmits.ts export function emit( instance: ComponentInternalInstance, event: string, ... rawArgs: any[] ) { const props = instance.vnode.props || EMPTY_OBJ ...... let handlerName let handler = props[(handlerName = toHandlerKey(event))] || // also try camelCase event handler (#2249) props[(handlerName = toHandlerKey(camelize(event)))] // for v-model update:xxx events, also trigger kebab-case equivalent // for props passed via kebab-case if (! handler && isModelListener) { handler = props[(handlerName = toHandlerKey(hyphenate(event)))] } if (handler) { callWithAsyncErrorHandling( handler, instance, ErrorCodes.COMPONENT_EVENT_HANDLER, args ) } ...... }Copy the code

With a custom directive, we can get the vndoe object, which seems to be a lot of work. Although the website says that you should keep everything except EL read-only, I’m not willing to give it a try. By assigning the corresponding event to vnode.props, yup!! With this method, you can actually listen to $emit(‘click’) inside the component.

Const app = vue.createApp ({}) // Register a global custom directive 'V-focus' app.directive('track', {// When a bound element is mounted into the DOM... Created (el,binding,vnode){vnode.props. OnClick =function(){console.log(' test.log ')}}})Copy the code

Of course, don’t be too happy. In the process of testing, we found a strange phenomenon that when there is props passing, the corresponding events can be directly assigned. However, when an instance doesn’t have props, I perform an operation that ultimately doesn’t work!

//vnode.props===null -----true vnode.props={} vnode.props. OnClick =function(){console.log(' test.log ')}Copy the code

It seems that we can go further ~ through the reverse lookup of patchEvent- >patchProp- >hostPatchProp, we finally locate the cause: At the beginning, WE got the index of props. When props was initialized to null,hostPatchProp could not be executed. When props was object, we modified props in invokeDirectiveHook stage, and hostPatchProp could read me We later added the onClick property and stored the corresponding event through patchEvent or registered the native event.

// packages\runtime-core\src\renderer.ts const mountElement = ( vnode: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { let el: RendererElement let vnodeHook: VNodeHook | undefined | null const { type, props, shapeFlag, transition, patchFlag, If (dirs) {invokeDirectiveHook(vnode, null, parentComponent, 'created') } // props if (props) { for (const key in props) { if (! isReservedProp(key)) { hostPatchProp( el, key, null, props[key], isSVG, vnode.children as VNode[], parentComponent, parentSuspense, unmountChildren ) } } if ((vnodeHook = props.onVnodeBeforeMount)) { invokeVNodeHook(vnodeHook, parentComponent, vnode) } } } }Copy the code

The above is vue3.0 burial point scheme exploration, in fact, in use and 2.0 instruction burial point is the same, just looking for an alternative $on.

decorator

All of the above are embedded solutions based on VUE capabilities, which cannot be generalized to other application frameworks. Although buried point scheme has code buried point, visual buried point, no buried point and other three types of scheme. However, in business scenarios, code burying point is often adopted, with low development cost, close to demand and high feasibility. But code buries often require adding extra code to events, intruding on business logic, which we don’t want to see. Thanks to es6’s introduction of decorators, it is easy to separate buried code from business code in the form of decorators, rather than writing both in the same function body.

Export default {methods: {@track(someConfig) onClick() {console.log(' trigger clik')}}}Copy the code

summary

This article mainly explores some related codes of $on in VUe2.0, and a scheme of instruction burying point under VUe3.0. If there is a better plan, also hope to solve the confusion. If there is any misunderstanding in this article, please comment on it.