从零学源码 | 发布订阅

400 阅读15分钟

从零学源码 _ 发布订阅.png

一、事件发布订阅

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源码阅读

github地址

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')
        })
    })
})

mitt-1.jpg

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]);
})

mitt-2.jpg

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]);
})

mitt-3.jpg

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]);
})

mitt-4.jpg

mitt-5.jpg

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]);
})

mitt-6.jpg

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);
})

mitt-7.jpg

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)判断mittoff方法是否能移除指定类型的事件处理程序

(3)判断mittoff方法移除事件处理程序时区分类型的大小写

(4)判断mittoff方法移除事件处理程序时,如果有传event,每次只移除第一个匹配上事件处理程序

此处用到了>>>,当indexOf的值是-1时,将其转换成一个较大的值,避免将handlers直接清空。因为按照逻辑,当handler未传值的时候,需要调用all!.set(type, [])这行代码。

mitt-8.jpg

mitt-9.jpg

mitt-10.jpg

(5)判断mittoff方法移除事件处理程序时,如果没有传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)判断mittemit方法能为类型调用事件处理程序

mitt-11.jpg

mitt-12.jpg

(3)判断mittemit方法调用事件处理程序的时候区分类型的大小写

mitt-13.jpg

(4)判断mittemit方法中是否有*事件处理程序并调用它

mitt-14.jpg

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中是这样初始化的,结合上面理解就是:

EventsRecord类型,规定了key的类型只能是string或是symbol,如baz:bat!Symbol('eventType')等,value的类型是不确定的。keyOf Events就是取到foo/constructor......

Handler<Events[keyof Events]是为了取到对应EventsHandler

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!);
            })
    }
}

mitt-15.jpg

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接口里面有一个属性和三个方法,分别是:allonoffemit

// 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表示KeyEvents里面的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源码阅读

github地址

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();
})

tiny-emitter-1.jpg

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');
});

tiny-emitter-2.jpg

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 在functionprototype对象上添加方法
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;
  },
}

tiny-emitter-3.jpg

tiny-emitter-4.gif

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);
  },
}

tiny-emitter-5.gif

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方法用于移除订阅。

第一步,先判断对应名称的evtscallback是否存在;

第二步,遍历evts,判断fnfn._是否与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

github地址

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调用时会传入两个参数,分别是eventfn。其中,eventstring类型或是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
}

五、mitttiny-emittervue events比较

方法mitttiny-emittervue events
订阅事件ononVue.prototype.$on
取消订阅事件offoffVue.prototype.$off
订阅一次事件onceVue.prototype.$once
触发事件emitemitVue.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传入的是namecallbackvue events传入的是eventfn

(2)均定义了一个新的方法,在该方法中,先调用各自的off方法,然后再使用apply调用传入的回调函数。

4.2 不同点

(1)tiny-emitter入参多了一个ctx

六、收获

1、事件发布订阅的实现思路

通过阅读和调试mitttiny-emittervue events三个事件订阅发布的源码实现,对于事件发布订阅有了更加清晰的认识。三者的实现思路大同小异,都包括了订阅事件、触发事件和取消订阅这三个基本功能。其中,ting-emittervue events还可以仅订阅一次事件,实现思路则是将传入的回调函数绑定到一个新的函数上,在触发事件的时候,先执行取消订阅的方法,然后再通过apply方法去调用原本要调用的回调函数。

2、单元测试的相关知识

3、TypeScript的相关知识

3、代码调试

面对陌生的源码,可以尝试先阅读test目录下面的jsts文件,然后再阅读源码中的核心部分。

七、参考

1、单元测试

chai API

Sinon API

sinon-chai

2、TypeScript

知乎 TypeScript 类型声明 与 进阶

知乎 Typescript高级类型Record

TypeScript入门教程 函数的类型

TypeScript入门教程 类型别名

TypeScript 数据类型

TypeScript中文手册 高级类型