preface

In VUe3, the $ON, $OFF, and $once instance methods have been removed, and component instances no longer implement event-triggering interfaces, because EventBus is often the easiest solution in the short term, but in the long term it becomes a headache to maintain and is no longer officially supported. Vue3 recommends props, emits, provide/ Inject, vuex, and Pinia to suppress it. However, for small projects we still want to use EventBus, for which vue3 recommends using third-party libraries such as Mitt or Tiny-Emitter. This paper will analyze mitt.

Strongly recommended: Do not use it in large projectsEventBusDon’t torture your teammates! This article only does study analysis 😂😂😂

Introduction to the

Mitt has the following advantages:

  • Zero dependence, super small size, only after compression200b.
  • Provides the completetypescriptSupport, can automatically derive the parameter type.
  • Implementation based on closures, nothing annoyingthisObsession.
  • Written for browsers but also supports othersjavascriptRuntime, browser supportie9+(Need to introduceMapthepolyfill).
  • Framework-independent and can be used with any framework.

Based on using

import mitt from 'mitt'

const emitter = mitt()

// Subscribe to a specific event
emitter.on('foo'.e= > console.log('foo', e) )

// Subscribe to all events
emitter.on(The '*'.(type, e) = > console.log(type, e) )

// Publish an event
emitter.emit('foo', { a: 'b' })

// Unsubscribe according to the subscription function
function onFoo() {}
emitter.on('foo', onFoo)   // listen
emitter.off('foo', onFoo)  // unlisten

// Unsubscribe from events with the same name
emitter.off('foo')  // unlisten

// Cancel all events
emitter.all.clear()
Copy the code

Mitt only exposes the ON, OFF, and EMIT apis, as well as the All instance (which is a map that stores the mapping of eventName => Handlers).

The basic usage is similar to vue2. X, except that unsubscribing is emitter.all.clear(), while vue2. X uses $off(), which is better.

Unfortunately, there is no API wrapper like $once in VUe2, but it is easy to implement manually.

Used in TS

import mitt from "mitt";

type Events = {
  foo: string;
  bar: number;
};

// Provide generic parameters for Emitter to automatically infer parameter types
const emitter = mitt<Events>();

// 'e' is inferred to be a string
emitter.on("foo".(e) = > {
  console.log(e);
});

// ts error: Arguments of type string cannot be assigned to arguments of type 'number'
emitter.emit("bar"."xx");

// ts error: otherEvent does not exist in the key with Events
emitter.on("otherEvent".() = > {
  //
});
Copy the code

Mitt passes in the generic Events parameter so that we can automatically infer the type of the parameter in the callback function when writing code, and also have the code prompt for the response when typing the event name.

Integrated vue3

There are many ways to integrate VUe3 and MITT. The following are recommended and their advantages and disadvantages are analyzed.

export/import

// @/utils/emitter.ts
import mitt from "mitt";

type Events = {
  foo: string;
  bar: number;
};

const emitter = mitt<Events>();
export default emitter;

// main.ts
import emitter from "@/utils/emitter";

emitter.on('foo'.(e) = > {
    console.log(e)
})

emitter.emit('foo'."hello");
Copy the code

This is the most classic integration approach and is suitable for a global event bus.

  • Advantages: Do not need to do other processing can be obtainedtypescript, can also be used in non-components.
  • Cons: Needed in useimport, only at the global level, not the component level.

Mount for use on globalProperties

// main.ts
import emitter from "@/utils/emitter";

// Mount globally
app.config.globalProperties.$emitter = emitter

/ / because must expand ComponentCustomProperties type to get tips
declare module "vue" {
  export interface ComponentCustomProperties {
    $emitter: typeofemitter; }}// Non-setup script syntax sugar used in app.vue
<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  ...
  mounted() {
    this.$emitter.on("foo".() = > {
      //do something}); },... }); </script>// Setup script syntax sugar used in app. vue
<script lang="ts" setup>
import { getCurrentInstance } from "vue";
constinstance = getCurrentInstance(); instance? .proxy? .$emitter.on("foo".() = > {
  // dosomething
});
Copy the code

Unlike in vue2 by expanding the prototype prototype to make its globally available, in vue3 need to set up the app. Config. GlobalProperties.

  • Advantage: Not used when the component is in useimport/export
  • Disadvantages:
    1. Need to usedeclare module "vue"expandComponentCustomPropertiesTo gettsType hint.
    2. setup scriptGrammar sugar needs to be usedgetCurrentInstance().proxyIn order to obtain$emitter, more complicated.
    3. Only at the global level, not at the component level.

provide/inject

// Parent component parent.vue
<script lang="ts">
import { InjectionKey } from "vue";
import mitt, { Emitter } from "mitt";
type Events = {
  foo: string;
  bar: number;
};
// To have typed hints, you must export a typed key
export const emitterKey: InjectionKey<Emitter<Events>> = Symbol("emitterKey");
</script>

<script lang="ts" setup>
import { provide } from "vue";
const emitter = mitt<Events>();
provide(emitterKey, emitter);
</script>// Child component child.vue<script lang="ts" setup>
import { inject } from "vue";
import { emitterKey } from "./Parent.vue";

// The typed key allows ts to infer the type of emitter
constemitter = inject(emitterKey)! ; emitter.on("foo".(e) = > {
  // dosomething
});
</script>
Copy the code

In order to make our Emitter have type when injecting, we need to borrow InjectionKey to export a typed key. Setup-script syntax is not a standard module, so we can only add a common module to export.

  • Benefits: Component-level supporteventbus.
  • Disadvantages:
    1. insetup-scriptIn syntax, you need twoscriptTag to work with.
    2. Need to beexport/importExport imports a typedkeyTo support typing.

summary

Each of the three integration methods has its own strengths. If it is global, you can use import/export only, both in components and ordinary files. If you are too lazy to write import, you can extend it with globalProperties. Although the provide/inject notation is complex, it supports component-level Event-bus and is mandatory in some scenarios.

The source code to appreciate

Js version

function mitt(all) {
  all = all || new Map(a);return {
    all: all,
    on: function (type, handler) {
      var handlers = all.get(type);
      if (handlers) {
        handlers.push(handler);
      } else{ all.set(type, [handler]); }},off: function (type, handler) {
      var handlers = all.get(type);
      if (handlers) {
        if (handler) {
          handlers.splice(handlers.indexOf(handler) >>> 0.1);
        } else{ all.set(type, []); }}},emit: function (type, evt) {
      var handlers = all.get(type);
      if (handlers) {
        handlers.slice().map(function (handler) {
          handler(evt);
        });
      }
      handlers = all.get("*");
      if (handlers) {
        handlers.slice().map(function (handler) { handler(type, evt); }); }}}; }Copy the code

The above is the code I left behind after compiling, and you can see that the implementation is very concise, with less than 50 lines of code, and very easy to understand. Compared with the core logic that everyone can write out, I think the TS version is more suitable for everyone to learn.

Ts version

export type EventType = string | symbol;

// An event handler can take an optional event argument
// and should not return a value
export type Handler<T = unknown> = (event: T) = > void;
export type WildcardHandler<T = Record<string, unknown>> = (
  type: keyof T,
  event: T[keyof T]
) = > void;

// An array of all currently registered event handlers for a type
export type EventHandlerList<T = unknown> = Array<Handler<T>>;
export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<
  WildcardHandler<T>
>;

// A map of event types and their corresponding event handlers.
export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
  keyof Events | "*",
  EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
>;

export interface Emitter<Events extends Record<EventType, unknown>> {
  all: EventHandlerMap<Events>;

  on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
  on(type: "*".handler: WildcardHandler<Events>): void;

  off<Key extends keyof Events>(
    type: Key, handler? : Handler<Events[Key]> ):void;
  off(type: "*".handler: WildcardHandler<Events>): void;

  emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
  emit<Key extends keyof Events>(
    type: undefined extends Events[Key] ? Key : never) :void;
}

/**
 * Mitt: Tiny (~200b) functional event emitter / pubsub.
 * @name mitt
 * @returns {Mitt}* /
export default function mitt<Events extends Record<EventType.unknown> > (all? : EventHandlerMap
       ) :Emitter<Events> {
  type GenericEventHandler =
    | Handler<Events[keyof Events]>
    | WildcardHandler<Events>;
  all = all || new Map(a);return {
    /** * A Map of event names to registered handler functions. */
    all,

    /**
     * Register an event handler for the given type.
     * @param {string|symbol} type Type of event to listen for, or `'*'` for all events
     * @param {Function} handler Function to call in response to given event
     * @memberOf mitt* /
    on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
      const handlers: Array<GenericEventHandler> | undefined= all! .get(type);
      if (handlers) {
        handlers.push(handler);
      } else{ all! .set(type, [handler] asEventHandlerList<Events[keyof Events]>); }},/**
     * Remove an event handler for the given type.
     * If `handler` is omitted, all handlers of the given type are removed.
     * @param {string|symbol} type Type of event to unregister `handler` from, or `'*'`
     * @param {Function} [handler] Handler function to remove
     * @memberOf mitt* /
    off<Key extends keyof Events>(type: Key, handler? : GenericEventHandler) {const handlers: Array<GenericEventHandler> | undefined= all! .get(type);
      if (handlers) {
        if (handler) {
          handlers.splice(handlers.indexOf(handler) >>> 0.1);
        } else{ all! .set(type[]); }}},/**
     * Invoke all handlers for the given type.
     * If present, `'*'` handlers are invoked after type-matched handlers.
     *
     * Note: Manually firing '*' handlers is not supported.
     *
     * @param {string|symbol} type The event type to invoke
     * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
     * @memberOf mitt* /
    emit<Key extends keyof Events>(type: Key, evt? : Events[Key]) {lethandlers = all! .get(type);
      if (handlers) {
        (handlers as EventHandlerList<Events[keyof Events]>)
          .slice()
          .map((handler) = >{ handler(evt!) ; }); } handlers = all! .get("*");
      if (handlers) {
        (handlers as WildCardEventHandlerList<Events>)
          .slice()
          .map((handler) = > {
            handler(type, evt!) ; }); }}}; }Copy the code

Ts version of the code is very simple, from the source can learn how to write a type safe small library, is worth learning.

conclusion

I can’t help but feel that the code written by the masters is like poetry. It has all the guts in it, especially the implementation of source CODE TS, which is a great learning material for me and other typescript novices. Code word is not easy, if you see the officer feel helpful to you, then give me a thumbs-up 👍👍👍, the first thanks!