clipboard.js代码分析(2)-emitter

上一篇文章介绍了clipboard.js这个工具库中的第一个依赖select这个工具库主要完成了对任意DOM元素的复制到粘贴板的功能。此次介绍一下clipboard.js源码中的第二个依赖的轻型工具库tiny-emitter这个工具库主要用来实现一个简易的基于监听发布者模式的事件派发和接收器,代码通过个人es6改写后只有40行,没有依赖第三方库,实现的功能倒是比较强大的,并且能够根据实际状况方便的进行扩展。javascript

快速上手

在研究源码以前,先看一下最广泛的使用场景。vue

const Emitter = require('./emitter')

let emitter = new Emitter()

// on 一个事件

let sayHello = name => console.log(`hello, ${name}`)
emitter.on('helloName', sayHello)
// emit 一个事件

// emitter.emit('helloName', 'dongzhe')

// on一个带有做用域的同一个事件
let obj = {
    prefix: 'smith',
    thankName (name) {
        console.log(`hello, ${this.prefix}.${name}`)
        return `hello, ${this.prefix}.${name}`
    }
}

emitter.on('helloName', obj.thankName, obj)
emitter.emit('helloName', 'dongzhe')

// new other emitter 能够在这里分组 不一样的组能够有一样的eventName
let emitter1 = new Emitter()

let sayHaHa = name => console.log(`haha, ${name}`)
emitter1.on('helloName', sayHaHa)
// emit 一个事件

emitter1.emit('helloName', 'dongzhe')

能够看出,每个事件管理器都是一个对象,能够根据不一样的业务场景模块建立不一样的事件管理器,事件管理器最基本功能就是动态的订阅事件和派发事件,固然还能够取消事件。用于在同一主模块下的不一样子模块以及不一样主模块之间的通讯,支持动态绑定做用域。若是用过vue的父子组件事件通讯以及eventBus,对事件管理器应该不会陌生的。java

源码实现

事件管理模型主要由4个函数构成,git

  • on 用于订阅事件,一个事件订阅多个触发函数
  • emit 用于发布事件,发布时会以此触发事件订阅的函数
  • once 订阅的事件只触发一次
  • off 取消订阅事件,支持指定取消,批量取消和所有取消

代码结构es6

class E {
    constructor () {
        this.eventObj = {}
    }
    on () {}
    once () {}
    emit () {}
    off () {}
}

module.exports = E

Emitter对象存在一个事件对象,以键值对的形式保存事件名称和对应的触发事件。github

订阅事件 on

订阅事件就是把要触发的函数放到事件对应的对象里面,若是事件不存在,须要初始化一下便可。一个事件能够动态的订阅多个触发函数。并且支持指定做用域,能够远程调用任意模块的函数。segmentfault

on (eventName, callback, ctx) {
    // 一个eventName能够绑定多个事件
    (this.eventObj[eventName] || (this.eventObj[eventName] = [])).push({callback, ctx})
    return this
}

发布事件 emit

相对订阅事件的就是发布事件,发布事件接收事件的事件名和触发函数的参数,将对应事件订阅的触发函数依次执行便可,参数可使用es6rest操做符。数组

emit (eventName, ...args) {
    let eventArr = (this.eventObj[eventName] || []).slice()
    eventArr.forEach(ele => ele.callback.call(ele.ctx, args))
    return this
}

取消事件 off

相对订阅事件,也应该能够取消事件,取消事件能够有多种选择,能够指定取消事件订阅的某一个或者多个触发函数,也能够直接将整个事件都取消掉。取消事件接收取消的事件名称,和一个可选的函数对象或者函数对象数组(我本身增长的),若是传入了指定的触发函数对象,经过遍历全部触发的函数来过滤掉须要取消的触发函数,最后从新赋值便可。若是没有传触发函数,那么就认为取消整个订阅的事件,直接从全局的事件对象中删除订阅对象便可promise

off (eventName, callback) {
    if (Object.prototype.toString.call(callback) === "[object Array]") {
        callback.forEach(func => this.off(eventName, func))
        return this
    } 
    let liveEvents = []
    let obj = this.eventObj
    let eventArr = obj[eventName]
    // 若是没有callback 就删除掉整个eventName对象
    if (eventArr && callback) {
        liveEvents = eventArr.filter(ele => (ele.callback !== callback && ele.callback._ !== callback))
    }
    (liveEvents.length) ? obj[eventName] = liveEvents : delete obj[eventName]
    return this
}

其中最主要的就是下面这一行代码了,使用filter过滤掉须要取消的触发函数,ele.callback._ !== callback是为了兼容once后面立刻就说到。app

liveEvents = eventArr.filter(ele => (ele.callback !== callback && ele.callback._ !== callback))

一次触发 once

有的时候咱们只须要触发一次订阅的事件,好比用户刚登陆进来获取历史消息或者通知消息,触发一次后就不须要了,因此有了once函数,once函数主要的工做原理就是,在函数内部添加一个代理函数listener代理函数用来为触发函数作代理,作代理的目的是为了添加逻辑,这个逻辑就是在触发函数第一次执行的时候,就自动执行off函数,用来取消触发函数的逻辑。

let listener = (...args) => {
    this.off(eventName, listener)
    callback.apply(ctx, args)
}
// 由于listener是在callback上封装了一层 因此要规定一个能够找到callbak的规则
listener._ = callback

由于listener是在callback上封装了一层代理 因此要规定一个能够找到callback的规则,这样off函数在传入取消函数的时候,咱们能够顺利的用兼容的方式找到。
最后其实订阅的是这个代理函数listener

once (eventName, callback, ctx) {
    let listener = (...args) => {
        this.off(eventName, listener)
        callback.apply(ctx, args)
    }
    // 由于listener是在callback上封装了一层 因此要规定一个能够找到callbak的规则
    listener._ = callback
    return this.on(eventName, listener, ctx)
}

完整代码

我本身在原来的代码基础上用es6从新编写,并添加了一些逻辑,能够对比原来的代码来看,最后完整的代码以下

class E {
    constructor () {
        this.eventObj = {}
    }
    on (eventName, callback, ctx) {
        // 一个eventName能够绑定多个事件
        (this.eventObj[eventName] || (this.eventObj[eventName] = [])).push({callback, ctx})
        return this
    }
    once (eventName, callback, ctx) {
        let listener = (...args) => {
            this.off(eventName, listener)
            callback.apply(ctx, args)
        }
        // 由于listener是在callback上封装了一层 因此要规定一个能够找到callbak的规则
        listener._ = callback
        return this.on(eventName, listener, ctx)
    }
    emit (eventName, ...args) {
        let eventArr = (this.eventObj[eventName] || []).slice()
        eventArr.forEach(ele => ele.callback.call(ele.ctx, args))
        return this
    }
    off (eventName, callback) {
        if (Object.prototype.toString.call(callback) === "[object Array]") {
            callback.forEach(func => this.off(eventName, func))
            return this
        } 
        let liveEvents = []
        let obj = this.eventObj
        let eventArr = obj[eventName]
        // 若是没有callback 就删除掉整个eventName对象
        if (eventArr && callback) {
            liveEvents = eventArr.filter(ele => (ele.callback !== callback && ele.callback._ !== callback))
        }
        (liveEvents.length) ? obj[eventName] = liveEvents : delete obj[eventName]
        return this
    }
}

module.exports = E

结语

这只是一个比较简单的事件订阅发布器,但包含的核心思想仍是比较完整的,用到了面向对象,订阅发布者模式,代理模式等,并且能够根据本身的需求进行很方便的扩展,好比我扩展的批量取消,也能够添加批量订阅,甚至使用promise来封装异步触发,每个函数都返回了对象自己,能够完成链式调用,好比订阅完成后马上触发完成初始化等等。

相关文章
相关标签/搜索