资讯专栏INFORMATION COLUMN

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

MoAir / 2295人阅读

摘要:用于在同一主模块下的不同子模块以及不同主模块之间的通信,支持动态绑定作用域。如果用过的父子组件事件通信以及,对事件管理器应该不会陌生的。而且支持指定作用域,可以远程调用任意模块的函数。

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

快速上手

在研究源码之前,先看一下最普遍的使用场景。

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,对事件管理器应该不会陌生的。

源码实现

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

on 用于订阅事件,一个事件订阅多个触发函数

emit 用于发布事件,发布时会以此触发事件订阅的函数

once 订阅的事件只触发一次

off 取消订阅事件,支持指定取消,批量取消和全部取消

代码结构

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

module.exports = E

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

订阅事件 on

订阅事件就是把要触发的函数放到事件对应的对象里面,如果事件不存在,需要初始化一下即可。一个事件可以动态的订阅多个触发函数。而且支持指定作用域,可以远程调用任意模块的函数。

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

相对订阅事件,也应该可以取消事件,取消事件可以有多种选择,可以指定取消事件订阅的某一个或者多个触发函数,也可以直接将整个事件都取消掉。取消事件接收取消的事件名称,和一个可选的函数对象或者函数对象数组(我自己增加的),如果传入了指定的触发函数对象,通过遍历所有触发的函数来过滤掉需要取消的触发函数,最后重新赋值即可。如果没有传触发函数,那么就认为取消整个订阅的事件,直接从全局的事件对象中删除订阅对象即可

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后面马上就说到。

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来封装异步触发,每一个函数都返回了对象本身,可以完成链式调用,比如订阅完成后立刻触发完成初始化等等。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93133.html

相关文章

  • clipboard.js代码分析(3)- good-listener

    摘要:上一篇文章介绍了这个工具库中的第二个依赖,这个工具库主要完成了一个简易的事件订阅发布器。节点事件绑定判断一个元素是否是节点,是通过构造函数和属性来判断的。 上一篇文章介绍了clipboard.js这个工具库中的第二个依赖tiny-emitter,这个工具库主要完成了一个简易的事件订阅发布器。这次介绍一下clipboard.js源码中的最后一个依赖的轻型工具库good-listener,...

    objc94 评论0 收藏0
  • quill深入浅出

    摘要:与的关系既是表达文档,又表达文档修改。然后会监听事件,然后触发的方法,传入参数,然后在的方法中,会依据构建出对应的数组,与已有的合并,使当前保持最新。 背景分析/技术选型 quillAPI驱动设计,自定义内容和格式化,跨平台,易用. CKEditor功能强,配置灵活,ui漂亮,兼容性差 TinyMCE文档好,功能强,bug少,无外部依赖。 UEditor功能齐全,但是不维护了,依赖j...

    hlcfan 评论0 收藏0
  • clipboard.js代码分析(1)-select

    摘要:下面对它的实现一一分析。可以使用获取选中的内容也可以使用获取一个用户选择的范围。在这里完成了对用户选中内容的一些操作,而且在不是表单无法触发事件的时候,也可以在指定区域监听事件来实时获取选中的内容完成复制等功能。 项目中用到了选中复制功能 showImg(https://segmentfault.com/img/bVY7dH?w=400&h=78); 就是点击按钮,复制左侧的内容到剪切...

    li21 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<