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來封裝異步觸發,每個函數都返回了對象自己,能夠完成鏈式調用,好比訂閱完成後馬上觸發完成初始化等等。

相關文章
相關標籤/搜索