一、事件发布订阅
1、什么是事件发布订阅?
发布订阅简单理解就是先订阅消息,然后在发布消息时能够及时收到发布的消息。以vue为例:
假设有两个组件,分别是父组件A,子组件B,现在有如下需求:当点击子组件B中的某个按钮时,需要告诉父组件A,该按钮被点击了。示例代码中的$emit就是事件发布订阅中的其中一个方法。
<!-- Parent A -->
<template>
<ChildComponentB @handleChildClick="childClicked()" />
</template>
<script>
export default {
name: "ParentComponentA"
data() {
return {}
},
methods:{
childClicked() {
console.log("父组件知道了~~");
}
}
}
</script>
<!-- Child B -->
<template>
<button @click="clickChild()">点击子组件</button>
</template>
<script>
export default {
name: "ChildComponentB"
data() {
return {}
},
methods:{
clickChild() {
console.log("点击子组件~~");
this.$emit("handleChildClick");
}
}
}
</script>
2、mitt
2.1 引入
(1)使用import
import mitt from 'mitt'
var mitt = require('mitt')
(2)使用script标签引入
<script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script>
<!-- window.mitt -->
2.2 使用
(1)js
import mitt from 'mitt'
const emitter = mitt()
emitter.on('foo', e => console.log('foo', e))
emitter.on('*', (type, e) => console.log(type, e))
emitter.emit('foo', { a: 'b' })
emitter.all.clear()
function onFoo() {}
emitter.on('foo', onFoo)
emitter.off('foo', onFoo)
(2)ts
import mitt from 'mitt';
type Events = {
foo: string;
bar?: number;
}
const emitter = mitt<Events>();
emitter.on('foo', (e) => {});
emitter.emit('foo', 42);
import mitt, { Emitter } from 'mitt';
type Events = {
foo: string;
bar?: number;
}
const emitter = Emitter<Events> = mitt<Events>();
3、tiny-emitter
3.1 使用
(1)tiny-emitter
var Emittter = require('tiny-emitter');
var emitter = new Emitter();
emitter.on('some-event', function(arg1, arg2, arg3) {
//
})
emitter.emit('some-event', 'arg1 value', 'arg2 value', 'arg3 value');
(2)tiny-emitter/instance
var emitter = require('tiny-emitter/instance');
emitter.on('some-event', function(arg1, arg2, arg3) {
//
})
emitter.emit('some-event', 'arg1 value', 'arg2 value', 'arg3 value');
3.2 实例方法
(1)on(event, callback[, context])
on方法用于订阅事件。
(2)once(event, callback[, context])
once方法用于订阅事件一次。
(3)off(event[, callback])
off方法用于取消订阅一个事件或全部事件,如果没有传入callback,则取消订阅全部事件。
(4)emit(event[, arguments...])
emit方法用于触发一个事件。
4、vue events
4.1 使用
import Vue from "vue";
import App from "./App.vue";
const v = new Vue({
render: (h) => h(App),
});
v.$mount("#app");
v.$on(["event-one", "event-two", "event-three"], () => {
console.log("this is multi-event");
})
v.$on("event-one", () => {
console.log("this is single-event");
})
setTimeout(() => {
v.$emit("event-one");
}, 10000)
二、mitt源码阅读
1、mitt/test/index_test.ts
1.1 判断mitt默认对外暴露的是否是function类型
import chai, { expect } from 'chai';
// 注:代码原为 import mitt, { Emitter, EventHandlerMap } from '..';
// 提示 找不到模块 “..”或其相应的类型声明,后改为 '../src/index'
import mitt, { Emitter, EventHandlerMap } from '../src/index';
describe('mitt', () => {
it('should default export be a funcion', () => {
// chai 语法,表示检验类型是否与期望一致
expect(mitt).to.be.a('function');
});
})
1.2 判断mitt是否支持传入一个map
import chai, { expect } from 'chai';
import { spy } from 'sinon';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
it('should accept an optional event handler map', () => {
// chai 语法,判断传入一个空的Map,不会抛出异常
expect(() => mitt(new Map())).not.to.throw;
const map = new Map();
const a = spy();
const b = spy();
map.set('foo', [a, b]);
const events = mitt<{ foo: undefined }>(map);
events.emit('foo');
// sinon-chai 方法,判断只调用一次
expect(a).to.have.been.calledOnce;
expect(b).to.have.been.calledOnce;
});
1.3 判断mitt返回的对象中是否有all属性,且该属性是一个map对象
describe('mitt#', () => {
const eventType = Symbol('eventType')
type Events = {
foo: unknown;
constructor: unknown;
FOO: unknown;
bar: unknown;
Bar: unknown;
'baz:bat!': unknown;
'baz:baT!': unknown;
Foo: unknown;
[eventType]: unknown;
};
let events: EventHandlerMap<Events>, inst: Emitter<Events>;
beforeEach(() => {
events = new Map();
inst = mitt(events);
})
describe('properties', () => {
it('should expose the event handler map', () => {
expect(inst)
// 判断是否有某个属性
.to.have.property('all')
// 判断是否是 map类型
.that.is.a('map')
})
})
})
1.4 判断mitt返回的对象中是否有on属性,且它是一个function类型
describe('on()', () => {
it('should be a function', () => {
expect(inst)
.to.have.property('on')
.that.is.a('function')
})
})
1.5 判断on方法会为新传入的类型注册一个事件处理程序
it('should register handler for new type', () => {
const foo = () => {};
inst.on('foo', foo);
expect(events.get('foo')).to.deep.equal([foo]);
})
1.6 判断on方法会为任意字符串的类型注册一个事件处理程序
it('should register handlers for any type strings', () => {
const foo = () => {};
inst.on('constructor', foo);
expect(events.get('constructor')).to.deep.equal([foo]);
})
1.7 判断on方法会为已经存在的类型添加一个新的事件处理程序
it('should append handler for existing type', () => {
const foo = () => {};
const bar = () => {};
inst.on('foo', foo);
inst.on('foo', bar);
expect(events.get('foo')).to.deep.equal([foo, bar]);
})
1.8 判断on方法注册事件处理程序时会区分类型的大小写
it('should NOT normalize case', () => {
const foo = () => {};
inst.on('FOO', foo);
inst.on('Bar', foo);
inst.on('baz:baT!', foo);
expect(events.get('FOO')).to.deep.equal([foo]);
expect(events.has('foo')).to.equal(false);
expect(events.get('Bar')).to.deep.equal([foo]);
expect(events.has('bar')).to.equal(false);
expect(events.get('baz:baT!')).to.deep.equal([foo]);
})
1.9 on方法的type支持传入symbol类型
const eventType = Symbol('eventType')
// ......
it('can take symbol for event types', () => {
const foo = () => {};
inst.on(eventType, foo);
expect(events.get(eventType)).to.deep.equal(foo);
})
1.10 on方法可以重复添加相同的类型
it('should add duplicate listeners', () => {
const foo = () => {};
inst.on('foo', foo);
inst.on('foo', foo);
exepect(events.get('foo')).to.deep.equal([foo, foo]);
})
1.11 off方法
(1)判断mitt返回的对象中是否有off属性,且它是一个function类型
(2)判断mitt的off方法是否能移除指定类型的事件处理程序
(3)判断mitt的off方法移除事件处理程序时区分类型的大小写
(4)判断mitt的off方法移除事件处理程序时,如果有传event,每次只移除第一个匹配上事件处理程序
此处用到了>>>,当indexOf的值是-1时,将其转换成一个较大的值,避免将handlers直接清空。因为按照逻辑,当handler未传值的时候,需要调用all!.set(type, [])这行代码。
(5)判断mitt的off方法移除事件处理程序时,如果没有传event,将移除全部事件处理程序
describe('off()', () => {
it('should be a function', () => {
expect(inst)
.to.have.property('off')
.that.is.a('function');
})
it('should remove handlers for type', () => {
const foo = () => {};
inst.on('foo', foo);
inst.off('foo', foo);
expect(events.get('foo')).to.be.empty;
})
it('should NOT normalize case', () => {
const foo = () => {};
inst.on('FOO', foo);
inst.on('Bar', foo);
inst.on('baz:bat!', foo);
inst.off('FOO', foo);
inst.off('Bar', foo);
inst.off('baz:baT!', foo);
expect(events.get('FOO')).to.be.empty;
expect(events.get('foo')).to.equal(false);
expect(events.get('Bar')).to.be.empty;
expect(events.get('bar')).to.equal(false);
expect(events.get('baz:bat!')).to.have.lengthOf(1);
})
it('should remove only the first matching listener', () => {
const foo = () => {};
inst.on('foo', foo);
inss.on('foo', foo);
inst.off('foo', foo);
expect(events.get('foo')).to.deep.equal([foo]);
inst.off('foo', foo);
expect(evnets.get('foo')).to.deep.equal([]);
})
it('off("type") should remove all hanlders of the given type', () => {
const foo = () => {};
inst.on('foo', foo);
inst.on('foo', foo);
inst.on('bar', foo);
inst.off('foo');
expect(events.get('foo')).to.deep.equal([]);
expect(events.get('bar')).to.have.length(1);
inst.off('bar');
expect(events.get('bar')).to.deep.equal([]);
})
})
1.12 emit方法
(1)判断mitt返回的对象中是否有emit属性,且它是一个function类型
(2)判断mitt的emit方法能为类型调用事件处理程序
(3)判断mitt的emit方法调用事件处理程序的时候区分类型的大小写
(4)判断mitt的emit方法中是否有*事件处理程序并调用它
describe('emit()', () => {
it('should be a function', () => {
expect(inst)
.to.have.property('emit')
.that.is.a('function')
});
it('should invoke handler for type', () => {
const event = { a: 'b' };
inst.on('foo', (one, two?: unknown) => {
expect(one).to.deep.equal(event);
expect(two).to.be.an('undefined');
});
inst.emit('foo', event);
})
it('should NOT ignore case', () => {
const onFoo = spy(),
onFOO = spy();
events.set('Foo', [onFoo]);
events.set('FOO', [onFOO]);
inst.emit('Foo', 'Foo arg');
inst.emit('FOO', 'FOO arg');
expect(onFoo).to.have.been.calledOnce.and.calledWidth('Foo arg');
expect(onFOO).to.have.been.calledOnce.and.calledWidth('FOO arg');
})
it('should invoke * handlers', () => {
const star = spy(),
ea = { a: 'a' },
eb = { b: 'b' };
events.set('*', [star]);
inst.emit('foo', ea);
expect(star).to.have.been.calledOnce.and.calledWith('foo', ea);
star.resetHistory();
inst.emit('bar', eb);
expect(star).to.have.been.calledOnce.and.calledWith('bar', eb);
})
})
2、src/index.ts
2.1 核心方法mitt
export default function mitt<Events extends Record<EventType, unknown>>(
all?: EventHandlerMap<Events>
): Emitter<Events> {
type GenericEventHandler =
| Handler<Events[keyof Events]>
| WildcardHandler<Events>;
all = all || new Map();
return {
all,
on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
},
off<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
},
emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
}
}
}
在test/index_test.ts中是这样初始化的,结合上面理解就是:
Events是Record类型,规定了key的类型只能是string或是symbol,如baz:bat!、Symbol('eventType')等,value的类型是不确定的。keyOf Events就是取到foo/constructor......
Handler<Events[keyof Events]是为了取到对应Events的Handler。
const eventType = Symbol('eventType')
type Events = {
foo: unknown;
constructor: unknown;
FOO: unknown;
bar: unknown;
Bar: unknown;
'baz:bat!': unknown;
'baz:baT!': unknown;
Foo: unknown;
[eventType]: unknown;
};
let events: EventHandlerMap<Events>, inst: Emitter<Events>;
beforeEach(() => {
events = new Map();
inst = mitt(events);
})
2.2 on
on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
// 根据传入的 type 获得对应的 handlers
const handlers: Array<GenericEventHandler> | undefined = all.get(type);
if (handlers) {
// 如果 handlers 有值。则往数组里再添加一个
handlers.push(handler);
} else {
// 如果没有值,则往map里面添加一个,key 值为传入的 type,值则为一个 EventHandlerList
all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
}
}
2.3 off
off<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
// 根据传入的 type 获得对应的 handlers
const handlers: Array<GenericEventHandler> | undefined = all.get(type);
if (handlers) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
} else {
// 如果没有值,则往map里面添加一个,key 值为传入的 type,值则为一个 EventHandlerList
all!.set(type, []);
}
}
2.4 emit
emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
let handlers = 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!);
})
}
}
2.5 定义变量和接口
(1)使用type定义类型别名
// EventType 表明值类型可以是 string 或是 symbol
export type EventType = string | symbol;
// 将一个入参类型为T,无返回值的函数,起个新名字叫 Handler,其中 T 不确定是什么数据类型
export type Handler<T = unknown> = (event: T) => void;
// 将一个有两个入参,无返回值的函数,起个新名字叫 WildcardHandler,其中 T 的类型是 Record<string, unknown>
// 可以理解为 第一个是 key 的类型,第二个是 value 的类型
// keyof T 是 索引类型查询操作符,表示取出 T 中所有的属性
export type WildcardHandler<T = Record<string, unknown>> = (
type: keyof,
event: T[keyof T]
) => void;
// 将一个类型为 Handler 的数组,起一个新名字叫 EventHandlerList,其中 T 不确定是什么数据类型
export type EventHandlerList<T = unknown> = Array<Handler<T>>;
// 将一个类型为 WildEventHandler 的数组,起一个新名字叫 WildCardEventHandlerList,其中 T 不确定是什么数据类型
export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<WildEventHandler<T>>;
// 将一个 key 值为 Events 中某个属性或*,value 值为 EventHandlerList 或 WildCardEventHandlerList 的 map,起一个新名字叫 EventHandlerMap
// Recore<EventType, unknown> 等同于 Record<string, unknown> + Record<symbol, unknown>
export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
keyof Events | '*',
EventHandlerList<Events[keyof Event]> | WildCardEventHandlerList<Events>
>;
(2)使用interface定义一个接口
Emitter接口里面有一个属性和三个方法,分别是:all、on、off和emit;
// Recore<EventType, unknown> 等同于 Record<string, unknown> + Record<symbol, unknown>
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: WildEventHandler<Events>): void;
off<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
off(type: '*', handler: WildEventHandler<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;
}
其中:
第一,Key extends keyof Events表示Key从Events里面的key中取值,例如:
type Events = {
foo: unknown;
constructor: unknown;
FOO: unknown;
bar: unknown;
Bar: unknown;
'baz:bat!': unknown;
'baz:baT!': unknown;
Foo: unknown;
[eventType]: unknown;
};
// Key extends keyof Events 的值 可以是 foo、constructor、FOO......
第二,:后面的void表明几个方法都是没有返回值的;
第三,Handler<Events[Key]>表示需要传入相应key的事件处理程序。例如:
const foo = () => {};
// 此时是添加
inst.on('foo', foo);
// 此时是移除
inst.off('foo', foo);
(3)Record的例子
interface EmployeeType {
id: number
fullname: string
role: string
}
let employees: Record<number, EmployeeType> = {
0: { id: 1, fullname: "John Doe", role: "Designer" },
1: { id: 2, fullname: "Ibrahima Fall", role: "Developer" },
2: { id: 3, fullname: "Sara Duckson", role: "Developer" },
}
// 0: { id: 1, fullname: "John Doe", role: "Designer" },
// 1: { id: 2, fullname: "Ibrahima Fall", role: "Developer" },
// 2: { id: 3, fullname: "Sara Duckson", role: "Developer" }
三、tiny-emitter源码阅读
1、tiny-mitter/test/index.js
1.1 准备
var Emitter = require('../index');
var emitter = require('../instance');
var tape = require('tape');
1.2 订阅一个事件
test('subscribe to an event', function(t) {
var emitter = new Emitter();
emitter.on('test', function() {});
t.equal(emitter.e.test.length, 1, 'subscribe to event');
t.end();
})
1.3 订阅一个带有上下文的事件
test('subscribes to an event with context', function (t) {
var emitter = new Emitter();
var context = {
contextValue: true
};
emitter.on('test', function () {
t.ok(this.contextValue, 'is in context');
t.end();
}, context);
emitter.emit('test');
});
1.4 订阅只执行一次的事件
test('subscibes only once to an event', function (t) {
var emitter = new Emitter();
emitter.once('test', function () {
t.notOk(emitter.e.test, 'removed event from list');
t.end();
});
emitter.emit('test');
});
1.5 当订阅一次的事件时可以保持上下文
test('keeps context when subscribed only once', function (t) {
var emitter = new Emitter();
var context = {
contextValue: true
};
emitter.once('test', function () {
t.ok(this.contextValue, 'is in context');
t.notOk(emitter.e.test, 'not subscribed anymore');
t.end();
}, context);
emitter.emit('test');
});
1.6 触发一次事件
test('emits an event', function (t) {
var emitter = new Emitter();
emitter.on('test', function () {
t.ok(true, 'triggered event');
t.end();
});
emitter.emit('test');
});
1.7 将所有参数传递给事件监听器
test('passes all arguments to event listener', function (t) {
var emitter = new Emitter();
emitter.on('test', function (arg1, arg2) {
t.equal(arg1, 'arg1', 'passed the first argument');
t.equal(arg2, 'arg2', 'passed the second argument');
t.end();
});
emitter.emit('test', 'arg1', 'arg2');
});
1.8 取消订阅带有名称的所有事件
test('unsubscribes from all events with name', function (t) {
var emitter = new Emitter();
emitter.on('test', function () {
t.fail('should not get called');
});
emitter.off('test');
emitter.emit('test')
process.nextTick(function () {
t.end();
});
});
1.9 取消订阅带有名称和回调函数的单个事件
test('unsubscribes single event with name and callback', function (t) {
var emitter = new Emitter();
var fn = function () {
t.fail('should not get called');
}
emitter.on('test', fn);
emitter.off('test', fn);
emitter.emit('test')
process.nextTick(function () {
t.end();
});
});
1.10 订阅两次时取消订阅带有名称和回调的单个事件
test('unsubscribes single event with name and callback when subscribed twice', function (t) {
var emitter = new Emitter();
var fn = function () {
t.fail('should not get called');
};
emitter.on('test', fn);
emitter.on('test', fn);
emitter.off('test', fn);
emitter.emit('test');
process.nextTick(function () {
t.notOk(emitter.e['test'], 'removes all events');
t.end();
});
});
1.11 当订阅两次乱序时,取消订阅带有名称和回调的单个事件
test('unsubscribes single event with name and callback when subscribed twice out of order', function (t) {
var emitter = new Emitter();
var calls = 0;
var fn = function () {
t.fail('should not get called');
};
var fn2 = function () {
calls++;
};
emitter.on('test', fn);
emitter.on('test', fn2);
emitter.on('test', fn);
emitter.off('test', fn);
emitter.emit('test');
process.nextTick(function () {
t.equal(calls, 1, 'callback was called');
t.end();
});
});
1.12 移除另一个事件中的一个事件
test('removes an event inside another event', function (t) {
var emitter = new Emitter();
emitter.on('test', function () {
t.equal(emitter.e.test.length, 1, 'event is still in list');
emitter.off('test');
t.notOk(emitter.e.test, 0, 'event is gone from list');
t.end();
});
emitter.emit('test');
});
1.13 即使在事件回调中取消订阅,也会触发事件
test('event is emitted even if unsubscribed in the event callback', function (t) {
var emitter = new Emitter();
var calls = 0;
var fn = function () {
calls += 1;
emitter.off('test', fn);
};
emitter.on('test', fn);
emitter.on('test', function () {
calls += 1;
});
emitter.on('test', function () {
calls += 1;
});
process.nextTick(function () {
t.equal(calls, 3, 'all callbacks were called');
t.end();
});
emitter.emit('test');
});
1.14 在添加任何事件之前取消没有任何影响
test('calling off before any events added does nothing', function (t) {
var emitter = new Emitter();
emitter.off('test', function () {});
t.end();
});
1.15 触发尚未订阅的事件
test('emitting event that has not been subscribed to yet', function (t) {
var emitter = new Emitter();
emitter.emit('some-event', 'some message');
t.end();
});
1.16 取消订阅单个事件,该事件带有名称和回调,且该事件已被订阅一次
test('unsubscribes single event with name and callback which was subscribed once', function (t) {
var emitter = new Emitter();
var fn = function () {
t.fail('event not unsubscribed');
}
emitter.once('test', fn);
emitter.off('test', fn);
emitter.emit('test');
t.end();
});
1.17 暴露一个实例
test('exports an instance', function (t) {
t.ok(emitter, 'exports an instance')
t.ok(emitter instanceof Emitter, 'an instance of the Emitter class');
t.end();
});
2、tiny-emittter/index.js
2.1 定义一个function
function E () {
}
2.2 在function的prototype对象上添加方法
E.prototype = {
on: function (name, callback, ctx) {
},
once: function (name, callback, ctx) {
},
emit: function (name) {
},
off: function (name, callback) {
}
};
2.3 对外暴露
module.exports = E;
module.exports.TinyEmitter = E;
2.4 on方法
先判断this.e[name]是否有值,如果有值则将传入的name,callback放入对应的map中。如果name已经存在,则继续放入。
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
return this;
},
}
2.5 once方法
订阅只执行一次。
第一步,声明一个回调函数,回调函数中调用移除订阅的off方法;
第二步,调用生成订阅的on方法,此时将回调函数传入;
第三步,调用emit方法的时候,执行回调函数,移除订阅;
第四步,再次调用emit方法时,因为evtArr的数组为空,不执行后续操作。
E.prototype = {
once: function (name, callback, ctx) {
// 此处先定义一个 self 变量,将this的值赋给它,方便后续在listener方法中获取外层的this对象
// 并调用off方法,移除订阅
var self = this;
function listener () {
self.off(name, listener);
callback.apply(ctx, arguments);
};
listener._ = callback
return this.on(name, listener, ctx);
},
}
2.6 emit方法
如果不是once,则会直接调用on时传入的callback函数;如果是once,由于调用on方法时传入的callback是一个listener函数,在listener函数中先执行了off方法,再执行调用once时传入的callback函数。
E.prototype = {
emit: function (name) {
var data = [].slice.call(arguments, 1);
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
for (i; i < len; i++) {
// 此处调用 回调函数
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
return this;
},
}
2.7 off方法
off方法用于移除订阅。
第一步,先判断对应名称的evts和callback是否存在;
第二步,遍历evts,判断fn或fn._是否与callback严格相等,如果不是,则放入另外一个数组中,这个数组用于替换原先的evts,同时将原先的evts删除。
E.prototype = {
off: function (name, callback) {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
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]);
}
}
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
}
四、vue events源码阅读
1、core/instance/events.js
1.1 initEvents
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
1.2 add
function add (event, fn) {
target.$on(event, fn)
}
1.3 remove
function remove (event, fn) {
target.$off(event, fn)
}
1.4 createOnceHandler
function createOnceHandler (event, fn) {
const _target = target
return function onceHandler () {
const res = fn.apply(null, arguments)
if (res !== null) {
_target.$off(event, onceHandler)
}
}
}
1.5 updateComponentListeners
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
1.6 eventsMixin
与tiny-emitter类似,有$on、$once、$off和$emit方法,分别对应订阅事件、订阅一次事件、取消订阅事件和触发事件等功能。
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
}
1.6.1 订阅事件 Vue.prototype.$on
Vue.prototype.$on调用时会传入两个参数,分别是event和fn。其中,event为string类型或是string类型的数组,fn为一个Function。
(1)判断传入的event是否是数组,如果是,则遍历数组中的每个元素,调用vm.$on方法,订阅事件。
(2)如果不是数组,则判断vm._events中是否已经存在该事件,如果不存在,再将其放入vm._events中。
(3)判断传入的event字符串是否满足hookRE的正则校验,如果满足,则将vm._hasHookEvent的值设置为true。
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 {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// 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.6.2 订阅一次事件 Vue.prototype.$once
(1)定义一个on方法,函数内部执行两个操作,首先是调用vm.off方法,然后是使用fn.apply调用传入的fn方法。
(2)给on方法添加一个fn属性,将传入的fn赋值给该属性。
(3)调用vm.$on方法,订阅事件。
(4)返回vm。
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.6.3 取消订阅事件 Vue.prototype.$off
(1)判断传入的event是否是数组,如果是,则遍历数组的每个元素,调用vm.off方法,然后直接返回vm。
(2)判断如果是传入的订阅事件存在,则将订阅事件中依次从vm._events[event]中移除,
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null
return vm
}
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
1.6.4 触发事件 Vue.prototype.$emit
(1)从vm._events[event]中取出需要执行的订阅事件。
(2)遍历数组中的每个元素,调用invokeWithErrorHandling方法。
(3)invokeWithErrorHandling方法在src/core/util/error.js中定义。该方法中先判断传入的args是否有值,如果有值,则调用handler.apply(context, args),如果没有值,则调用handler.call(context),调用完之后对结果可能存在的异常进行捕获,并抛出相应的提示信息。
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}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
五、mitt、tiny-emitter和vue events比较
| 方法 | mitt | tiny-emitter | vue events |
|---|---|---|---|
| 订阅事件 | on | on | Vue.prototype.$on |
| 取消订阅事件 | off | off | Vue.prototype.$off |
| 订阅一次事件 | once | Vue.prototype.$once | |
| 触发事件 | emit | emit | Vue.prototype.$emit |
1、on方法
1.1 不同点
(1)订阅事件时的处理方式不同。
其中:
mitt是判断从all.get()中取出的handlers是否有值,如果有值,则往数组中继续添加传入的handlers,如果不存在,则使用all!.set方法添加一个新的key。
on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
// 根据传入的 type 获得对应的 handlers
const handlers: Array<GenericEventHandler> | undefined = all.get(type);
if (handlers) {
// 如果 handlers 有值。则往数组里再添加一个
handlers.push(handler);
} else {
// 如果没有值,则往map里面添加一个,key 值为传入的 type,值则为一个 EventHandlerList
all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
}
}
tiny-emitter则是判断订阅事件是否存在,如果不存在,则将其添加到数组中。
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
return this;
},
}
vue events的判断与tiny-emitter类似,但vue events支持同时订阅多个事件。
2、off方法
2.1 不同点
(1)取消订阅时的处理方式不同。
其中:
mitt是使用splice方法将第一个满足条件的订阅事件删除。
off<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
// 根据传入的 type 获得对应的 handlers
const handlers: Array<GenericEventHandler> | undefined = all.get(type);
if (handlers) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
} else {
// 如果没有值,则往map里面添加一个,key 值为传入的 type,值则为一个 EventHandlerList
all!.set(type, []);
}
}
tiny-emitter是将同一事件标识中的事件遍历一遍,如果与取消订阅时传入的fn不同,则将其丢入定义好的一个新数组中。遍历完成之后,将新数组赋值给对应事件标识,并将原数组删除。而
E.prototype = {
off: function (name, callback) {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
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]);
}
}
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
}
vue events则是先判断传入的是否时数组,如果不是,则遍历判断是否与取消订阅时传入的fn相同,如果相同,则使用splice方法将其删除。
3、emit方法
3.1 不同点
(1)触发事件时的处理方式不同。
其中:
mitt是通过get方法从数组中取出需要执行的回调函数,并依次执行。然后判断是否有需要全部事件都执行的回调函数,如果有,则依次执行。
emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
let handlers = 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!);
})
}
}
tiny-emitter是使用evtArr[i].fn.apply去做处理。
E.prototype = {
emit: function (name) {
var data = [].slice.call(arguments, 1);
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
for (i; i < len; i++) {
// 此处调用 回调函数
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
return this;
},
}
vue events则是从vm._events中取出需要执行的回调函数,遍历其中每个元素,使用invokeWithErrorHandling对其进行处理,并将执行过程中的异常捕获,并抛出相应的提示信息。
4、once方法
4.1 相同点
(1)均传入了事件名称和回调函数两个参数。
其中,tiny-emitter传入的是name和callback,vue events传入的是event和fn。
(2)均定义了一个新的方法,在该方法中,先调用各自的off方法,然后再使用apply调用传入的回调函数。
4.2 不同点
(1)tiny-emitter入参多了一个ctx
六、收获
1、事件发布订阅的实现思路
通过阅读和调试mitt、tiny-emitter和vue events三个事件订阅发布的源码实现,对于事件发布订阅有了更加清晰的认识。三者的实现思路大同小异,都包括了订阅事件、触发事件和取消订阅这三个基本功能。其中,ting-emitter和vue events还可以仅订阅一次事件,实现思路则是将传入的回调函数绑定到一个新的函数上,在触发事件的时候,先执行取消订阅的方法,然后再通过apply方法去调用原本要调用的回调函数。
2、单元测试的相关知识
3、TypeScript的相关知识
3、代码调试
面对陌生的源码,可以尝试先阅读test目录下面的js或ts文件,然后再阅读源码中的核心部分。