What does the interviewer ask about the publish and subscribe model?

This article comes from the eighth issue of notes submitted by @ simonezhou's little sister. Interviewers often ask about publish, subscribe and observer models, which are also commonly used in our daily development. This article describes the source code related to mitt, tiny emitter and Vue eventBus.

Source address

  1. mitt: https://github.com/developit/mitt
  2. tiny-emitter: https://github.com/scottcorgan/tiny-emitter

1. Interpretation of MIT source code

1.1 package.json project build package (it will not be studied in detail when applied to the package, just keep an impression)

Execute npm run build:

// 
"scripts": {
    ...
    "bundle": "microbundle -f es,cjs,umd",
    "build": "npm-run-all --silent clean -p bundle -s docs",
    "clean": "rimraf dist",
    "docs": "documentation readme src/index.ts --section API -q --parse-extension ts",
   ...
 },
  • Using NPM run all (a cli tool to run multiple NPM scripts in parallel or sequential: https://www.npmjs.com/package/npm-run-all )Command execution
  • clean command, use rimraf (the UNIX command RM - RF for node https://www.npmjs.com/package/rimraf )Delete dist file path
  • The micro bundle (the zero configuration bundle for tiny modules, powered by rollup https://www.npmjs.com/package/microbundle )Packaging
  • The microbundle command specifies format: es, CJS, UMD, package.json, and specifies the soucre field as the package entry js:
{
 "name": "mitt",          // package name
  ...
  ...
  "module": "dist/mitt.mjs",    // ES Modules output bundle
  "main": "dist/mitt.js",      // CommonJS output bundle
  "jsnext:main": "dist/mitt.mjs",   // ES Modules output bundle
  "umd:main": "dist/mitt.umd.js",  // UMD output bundle
  "source": "src/index.ts",     // input
  "typings": "index.d.ts",     // TypeScript typings directory
  "exports": {
    "import": "./dist/mitt.mjs",    // ES Modules output bundle
    "require": "./dist/mitt.js",  // CommonJS output bundle
    "default": "./dist/mitt.mjs"  // Modern ES Modules output bundle
  },
  ...
}

1.2 how to debug, view and analyze?

Use the microbundle watch command to add script and execute npm run dev:

"dev": "microbundle watch -f es,cjs,umd"

Add an entry corresponding to the directory, such as test.js, and execute the node test.js test function:

const mitt = require('./dist/mitt');

const Emitter = mitt();

Emitter.on('test', (e, t) => console.log(e, t));

Emitter.emit('test', { a: 12321 });

The corresponding source code src/index.js can still be viewed by adding related log s. Repackaging will be triggered after code changes

1.3. TS declaration

For example, foo events can be defined. The parameters in the callback function must be of string type. Imagine how the source code TS defines them:

import mitt from 'mitt';

// Key is the event name, and the corresponding attribute of key is the parameter type of callback function 
type Events = {
  foo: string;
  bar?: number; // The corresponding event is not allowed to pass parameters
};

const emitter = mitt<Events>(); // inferred as Emitter<Events>

emitter.on('foo', (e) => {}); // 'e' has inferred type 'string'

emitter.emit('foo', 42); // Error: Argument of type 'number' is not assignable to parameter of type 'string'. (2345)

emitter.on('*', (type, e) => console.log(type, e) )

TS definition in the source code (key sentences):

export type EventType = string | symbol;

// Handler defines the callback function for events (except * events)
export type Handler<T = unknown> = (event: T) => void;

// WildcardHandler is defined for the event * callback function
export type WildcardHandler<T = Record<string, unknown>> = (
 type: keyof T,   // keyof T, event name
 event: T[keyof T]  // T[keyof T], callback function input parameter type corresponding to event name
) => void;


export interface Emitter<Events extends Record<EventType, unknown>> {
 // ...

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

 // ...
 emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
  // This sentence is mainly compatible with events without parameters. If the callback corresponding to the event must pass parameters, if it is not passed in use, it will hit never, as shown in the following figure
 emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
}

The following are TS errors that will be reported:

The following is true:

1.4 main logic

  1. The whole is a function. The input is an event Map and the output is all. There are also on, emit and off event methods:
export default function mitt<Events extends Record<EventType, unknown>>(
  // Support all initialization
 all?: EventHandlerMap<Events>
): Emitter<Events> {
  // A Map (all) is maintained internally. The Key is the event name and the Value is the Handler callback function array
  all = all || new Map();
  return {
   all,   // All events & Event correspondence method
   emit,  // Trigger event
   on,   // Subscription event
   off   // Unregister Event 
  }
}
  1. on is event subscription, and the corresponding Handler is push ed into the Handler callback function array of the corresponding event map (you can be familiar with the map related APIs below) https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map ):
on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
  // Map get
 const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
  // If it has been initialized, it is an array and can be push ed directly
 if (handlers) {
  handlers.push(handler);
 }
  // If the event is registered for the first time, set the new array
 else {
  all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
 }
}
  1. off is event logoff. From the Handlers of the corresponding event Map, split:
off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
  // Map get
 const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
  // If there is an event list, enter; if not, ignore
 if (handlers) {
    // Split the handler event out of the array
    // This is to remove the first handler found, so if you subscribe to it multiple times, only the first handler will be removed
    // Handlers. Indexof (handler) > > > 0, > > > is unsigned displacement
    // It doesn't just convert non numbers to number, it converts them to numbers that can be expressed as 32-bit unsigned ints
  if (handler) {
   handlers.splice(handlers.indexOf(handler) >>> 0, 1);
  }
    // If the corresponding Handler is not transferred, all subscriptions corresponding to the event will be cleared
  else {
   all!.set(type, []);
  }
 }
}
  1. If emit is event trigger, the Handlers of the event Map will be read and triggered one by one. If * all events are subscribed, the Handlers of * will be read and triggered one by one:
emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
  // Get the Handlers of the corresponding type
 let handlers = all!.get(type);
 if (handlers) {
  (handlers as EventHandlerList<Events[keyof Events]>)
   .slice()
   .map((handler) => {
    handler(evt!);
   });
 }

  // Get * corresponding Handlers
 handlers = all!.get('*');
 if (handlers) {
  (handlers as WildCardEventHandlerList<Events>)
   .slice()
   .map((handler) => {
    handler(type, evt!);
   });
 }
}

Why use slice().map() instead of forEach() to trigger? For details: https://github.com/developit/mitt/pull/109 Specifically, you can copy the relevant code for debugging. If you directly replace it with forEach, the emit triggered for the following example is wrong:

import mitt from './mitt'

type Events = {
  test: number
}

const Emitter = mitt<Events>()
Emitter.on('test', function A(num) {
  console.log('A', num)
  Emitter.off('test', A)
})
Emitter.on('test', function B() {
  console.log('B')
})
Emitter.on('test', function C() {
  console.log('C')
})

Emitter.emit('test', 32432) // Trigger events A and C, and B will be missed
Emitter.emit('test', 32432) // Trigger B, C, this is correct

// Explanation:
// During forEach, the off operation is triggered during the Handlers loop
// According to this example, A is the first one to be registered, so the first one will be dropped by slice
// Because array is a reference type, after slice, the B function will become the first
// But at this time, the traversal has reached the second, so the B function will be omitted

// Solution:
// Therefore, make a shallow copy of the array by []. slice(). The Handlers of off are handled differently from those in the current loop
// []. slice.forEach() has the same effect. If you use map, you don't feel very semantic

1.5 summary

  • Flexible use of TS keyof
  • undefined extends Events[Key] ? Key: never, which is the condition type of TS( https://www.typescriptlang.org/docs/handbook/2/conditional-types.html )
  • undefined extends Events[Key] ? Key: never. When we want the compiler not to capture the current value or type, we can return the never type. Never represents the type of value that never exists
// From lib.es5.d.ts definition in typescript

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends null | undefined ? never : T;

// If the value of T contains null or undefined, it will never indicate that this logic is not allowed, otherwise the type of T itself will be returned
  • mitt's event callback function has only one parameter, not multiple. How to be compatible with multiple parameters, the official recommendation is to use object (object is recommended and powerful). This design has higher scalability and is more worthy of recommendation.

2. Interpretation of tiny emitter source code

2.1 main logic

  1. All methods are mounted in the prototype of E, and a total of four event methods of once, emit, off and on are exposed:
function E () {
  // Keep this empty so it's easier to inherit from
  // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}

// All events are mounted on this.e, which is an object
E.prototype = {
 on: function (name, callback, ctx) {},
  once: function (name, callback, ctx) {},
 emit: function (name) {},
  off: function (name, callback) {}
}

module.exports = E;
module.exports.TinyEmitter = E;
  1. Once subscribes to an event. When it is triggered once, it will be destroyed:
once: function (name, callback, ctx) {
  var self = this;
  // Construct another callback function. After calling, destroy the callback
  function listener () {
    self.off(name, listener);     // Destroy
    callback.apply(ctx, arguments);  // implement
  };

  listener._ = callback
  
  // The on function returns this, so it can be called chained
  return this.on(name, listener, ctx); // Subscribe to the callback function of this construct
}
  1. on event subscription
on: function (name, callback, ctx) {
  var e = this.e || (this.e = {});

  // Simply push in, and there is no de duplication here, so the same callback function can be subscribed multiple times
  (e[name] || (e[name] = [])).push({
    fn: callback,
    ctx: ctx
  });

  // Return this, which can be called in a chain
  return this;
}
  1. off event destroy
off: function (name, callback) {
  var e = this.e || (this.e = {});
  var evts = e[name];
  var liveEvents = []; // Save valid hanlder

 // If the callback passed is hit, it will not be put into liveEvents
  // Therefore, the destruction here is to destroy all the same callback s at one time, which is different from MIT
  if (evts && callback) {
    for (var i = 0, len = evts.length; i < len; i++) {
      if (evts[i].fn !== callback && evts[i].fn._ !== callback)
        liveEvents.push(evts[i]);
    }
  }

  // Remove event from queue to prevent memory leak
  // Suggested by https://github.com/lazd
  // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910

  // If there is no handler, the corresponding event name can also be delete d
  (liveEvents.length)
    ? e[name] = liveEvents
    : delete e[name];

  // Return this, which can be called in a chain
  return this;
}
  1. emit event trigger
emit: function (name) {
  // Take all remaining parameters except the first bit
  var data = [].slice.call(arguments, 1);
  
  // slice() shallow copy
  var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
  var i = 0;
  var len = evtArr.length;

  // The loop triggers the handler one by one and passes data into it
  for (i; i < len; i++) {
    evtArr[i].fn.apply(evtArr[i].ctx, data);
  }

  // Return this, which can be called in a chain
  return this;
}

2.2 summary

  • return this, which supports chain calls
  • When the emit event is triggered, []. slice.call(arguments, 1) removes the first parameter, obtains the remaining parameter list, and then uses apply to call
  • When subscribing to on events, {fn, ctx} are recorded. fn is a callback function, and ctx supports binding context

3. Comparison between mitt and tiny emitter

  • In TS static type verification, mitt > tiny emitter is more friendly for development. Tiny emitter supports multi parameter calls for the management of callback function parameters. However, mitt advocates the use of object management, which makes mitt more friendly and standardized in design
  • In the destruction of off events, tiny emitter is handled differently from MIT. Tiny emitter will destroy all the same callback s at one time, while MIT only destroys the first one
  • MIT does not support the once method, and tiny e mitt er supports the once method
  • MIT supports * full event subscription, while tiny e mitt er does not

4. Vue eventBus event bus (3.x abolished, 2.x still exists)

  • About events processing: https://github.com/vuejs/vue/blob/dev/src/core/instance/events.js
  • Event related initialization: https://github.com/vuejs/vue/blob/dev/src/core/instance/index.js
  1. Initialization process
// index.js calls initMixin method to initialize_ events object
initMixin(Vue)

// event.js defines the initEvents method
// vm._events saves all events & event callback function, which is an object
export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  // ...
}

// index.js calls eventsMixin to mount related event methods to Vue.prototype
eventsMixin(Vue)

// event.js defines the eventsMixin method
export function eventsMixin (Vue: Class<Component>) {
  // event subscriptions 
 Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
  // The event subscription is executed once
  Vue.prototype.$once = function (event: string, fn: Function): Component {}
  // Event unsubscribe
  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
  // Event trigger
  Vue.prototype.$emit = function (event: string): Component {}
}
  1. $on event subscription
// event is a string or an array of strings
// Note: you can subscribe to the same callback function for multiple events at once
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  const vm: Component = this
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn)
    }
  } else {
    // In essence, it corresponds to event and push corresponds to fn
    (vm._events[event] || (vm._events[event] = [])).push(fn)
    
    // The following will not be expanded first, but the call description of hookEvent
    // optimize hook:event cost by using a boolean flag marked at registration
    // instead of a hash lookup
    if (hookRE.test(event)) {
      vm._hasHookEvent = true
    }
  }
  return vm
}
  1. $once event subscription & execute once
// The packaging layer is on, which contains unsubscribe operations and call operations
// The subscription is the wrapped on callback function
Vue.prototype.$once = function (event: string, fn: Function): Component {
  const vm: Component = this
  function on () {
    vm.$off(event, on)
    fn.apply(vm, arguments)
  }
  on.fn = fn
  vm.$on(event, on)
  return vm
}
  1. $off event unsubscribe
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
  const vm: Component = this
  
  // If no parameters are passed, all events will be unsubscribed and cleared directly
  if (!arguments.length) {
    vm._events = Object.create(null)
    return vm
  }
  
  // There is an event array. Call yourself one by one
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$off(event[i], fn)
    }
    return vm
  }
  
  // If the following is a non array event name and a single event, get the callbacks of the subscription corresponding to the event
  const cbs = vm._events[event]
  // If callbacks is empty, do nothing
  if (!cbs) {
    return vm
  }
  // If the fn passed in is empty, it means that all callbacks of this event are unsubscribed
  if (!fn) {
    vm._events[event] = null
    return vm
  }
  // If callbacks is not empty and fn is not empty, a callback is unsubscribed
  let cb
  let i = cbs.length
  while (i--) {
    cb = cbs[i]
    // All callbacks subscribed to multiple times will be unsubscribed. All the same callbacks will be unsubscribed at one time
    if (cb === fn || cb.fn === fn) {
      cbs.splice(i, 1)
      break
    }
  }
  return vm
}
  1. $emit event triggered
Vue.prototype.$emit = function (event: string): Component {
  const vm: Component = this
  if (process.env.NODE_ENV !== 'production') {
    const lowerCaseEvent = event.toLowerCase()
    if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
      tip(
        `Event "${lowerCaseEvent}" is emitted in component ` +
        `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
        `Note that HTML attributes are case-insensitive and you cannot use ` +
        `v-on to listen to camelCase events when using in-DOM templates. ` +
        `You should probably use "${hyphenate(event)}" instead of "${event}".`
      )
    }
  }
  
  // Get the callbacks of this event
  let cbs = vm._events[event]
  if (cbs) {
    cbs = cbs.length > 1 ? toArray(cbs) : cbs
    // Get all parameters except the first bit
    const args = toArray(arguments, 1)
    const info = `event handler for "${event}"`
    // Traversal triggered one by one
    for (let i = 0, l = cbs.length; i < l; i++) {
      // The following is not expanded temporarily. This is the handling scheme for method call error exceptions in Vue
      invokeWithErrorHandling(cbs[i], vm, args, vm, info)
    }
  }
  return vm
}

The implementation logic is roughly the same as that of MIT and tiny e mitt er. It is also pubsub. The overall idea is to maintain an object or Map, put on into the array, emit is triggered one by one through circular traversal, and off is to find the corresponding handler and remove the array TODO:

  • Handling scheme for method call error exception in Vue: invokeWithErrorHandling
  • Application & principle of hookEvent

5. Appendix

  • rimraf: https://www.npmjs.com/package/rimraf
  • microbundle: https://www.npmjs.com/package/microbundle
  • package.json exports field: https://nodejs.org/api/packages.html#packages_conditional_exports
  • Map: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map
  • TS condition type: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html
  • TS Never: https://www.typescriptlang.org/docs/handbook/basic-types.html#never
  • TS keyof: https://www.typescriptlang.org/docs/handbook/2/keyof-types.html#the-keyof-type-operator
  • What is the JavaScript >>> operator and how do you use it? https://stackoverflow.com/questions/1822350/what-is-the-javascript-operator-and-how-do-you-use-it

Added by FlashHeart on Thu, 18 Nov 2021 08:52:19 +0200