DOM 当中有一系列事件,比如 click
, mousedown
等。添加这些事件回调的操作,前端程序员当然都是轻车熟路了。
假如要添加自定义事件呢?
添加事件
背景
比如一群小伙伴,就大雄,静香和小夫吧,结伴出去玩,约定在公园会和,先到公园的小伙伴,需要告诉其他人,我已经到了。而收到消息的小伙伴,在路上的,可能要加快步伐,还在挑衣服的,可能也要快点出门了。
这里有两个约定的事件,就是“到达”和“收到信息”,到达的小伙伴,需要发消息通知其他人;收到信息的小伙伴,要加快进度了。
用 JavaScript 的方式描述一下,每个小伙伴都需要监听“到达”和“收到信息”这两个事件,然后做出相应的反应,俗称回调函数。
核心代码
而 JavaScript 内部是没有“到达”和“收到信息”的事件的,这个时候,自定义事件就派上用场了,先上代码:
function EventCenter() {
this.handlers = {}
}
EventCenter.prototype = {
constructor: EventCenter,
listen: function(type, handler) {
if (this.handlers[type] === undefined) {
this.handlers[type] = []
}
this.handlers[type].push(handler)
},
trigger: function(type, ...args) {
const typeHandlers = this.handlers[type]
if (typeHandlers === undefined || typeHandlers.length === 0) {
return
}
for (let i = 0; i < typeHandlers.length; i++) {
typeHandlers[i].apply(this, args)
}
}
}
代码中,首先定义了构造函数 EventCenter
, 这个构造函数的实例,将帮助我们添加和处理自定义事件。
构造函数内部,首先声明了空对象 handlers
, 用来存放事件处理函数。
在 EventCenter
的 prototype
上,先将 constructor
改为 EventCenter
. 这样一来,new EventCenter() instanceof EventCenter
就会返回 true
, 说明 new
出来的结果确实是 EventCenter
实例。
listen
方法接受两个参数 type
和 handler
, type
是字符串,代表事件的类型,类似我们最熟悉的 click
; 而 handler
是函数,代表事件处理函数。listen
将 handler
添加到添加到 type
事件的处理函数集合中。
listener
内部首先判断 type
类型的处理函数集合 this.handlers[type]
是否存在,如果没有,就将其初始化为一个空数组。然后将 handler
添加到这个数组里面。
trigger
方法至少接受一个参数 type
, 它将触发 type
类型的所有事件处理函数。trigger
多余的参数,将传递给事件处理函数。
trigger
内部首先取得 type
事件的处理函数集合, 并将其赋值给 typeHandlers
. 试图触发一个没有被 listen
过的事件时,typeHandlers
是 undefined
, 所以要首先排除这种情况。接下来,调用 for
循环,依次执行该类型的事件处理函数,调用 apply
方法将 this
指向 trigger
的 this
, 将 trigger
函数的多余参数传递给事件处理函数。
应用一下
在这里,我们会将每个小伙伴都初始化为一个 EventCenter
实例,将"到达"事件定义为 arrive
,将“收到消息”事件定义为 message
. 小伙伴将处理 arrive
和 message
事件。
function sendMessage(message, list) {
if (!Array.isArray(list)) {
return
}
for (let i = 0; i < list.length; i++) {
list[i].trigger('message', message)
}
}
let daxiong = new EventCenter()
let xiaofu = new EventCenter()
let jingxiang = new EventCenter()
daxiong.listen('arrive', function() {
console.log('大雄到了,给其他小伙伴发消息。')
sendMessage('大雄:我到了。', [jingxiang, xiaofu])
})
jingxiang.listen('message', function(message) {
console.log('静香收到消息:' + message)
console.log('静香:我要赶紧出门了。')
})
xiaofu.listen('message', function(message) {
console.log('小夫收到消息:' + message)
console.log('小夫:切。')
})
daxiong.trigger('arrive')
运行结果如下:

函数 sendMessage
, 用来发送消息给小伙伴,接受两个参数 message
和 list
. message
是消息内容, list
代表一个名单,名单上的小伙伴将收到消息。
sendMessage
内部,首先判断 list
是不是数组,若不是,就终止函数执行。否则,调用 for
循环,依次触发 list
名单上每个小伙伴的 message
事件,并把 message
传递过去。
然后初始化三个 EventCenter
, 大雄,小夫和静香。
接下来,大雄调用 listen
方法,添加 arrive
的处理函数。大雄一到公园,将消息发送给静香和小夫。
调用静香的 listen
方法,为静香添加 message
的处理函数。
接着调用小夫的 listen
方法,为小夫添加 message
处理函数。
大雄到了,利用 daxiong.trigger('arrive')
触发大雄的到达事件。
移除事件
除了添加事件监听,常见的场景还有移除事件的监听。
在这里我们假设,小夫放了他俩鸽子,这个时候就要取消小夫的 message
事件。
核心代码
首先来完善 EventCenter
的代码:
EventCenter.prototype = {
... // 省略 EventCenter 已有的代码。
remove: function(type, handler) {
if (this.handlers[type] === undefined) {
return
}
this.handlers[type] = this.handlers[type].filter((item) => item !== handler)
}
}
remove
方法接收两个参数,一个是表示时间类型的 type
, 另一个 handler
则代表要删除的事件处理函数。
方法内部首先取得 type
类型事件的处理函数集合, 同样,这里也要判断这个集合是否存在。关键的删除语句,用的是 filter
方法,返回事件处理集合中除了 handler
的所有函数。
应用一下
来用一下,借助多啦 A 梦的时光机,回到一开始。这个时候,大雄还没到公园。
daxiong.listen('arrive', function() {
console.log('大雄到了,给其他小伙伴发消息。')
sendMessage('大雄:我到了。', [jingxiang, xiaofu])
})
jingxiang.listen('message', function(message) {
console.log('静香收到消息:' + message)
console.log('静香:我要赶紧出门了。')
})
const messageHandlerXf = function(message) {
console.log('小夫收到消息:' + message)
console.log('小夫:切。')
})
xiaofu.listen('message', messageHandlerXf)
// 小夫遇到胖虎,被强行拉去打棒球。
// 注意,他要开始鸽了。
xiaofu.remove('message', messageHandlerXf)
daxiong.trigger('arrive')
运行结果如下图:

在这里,大雄和静香的代码与之前的别无二致。小夫 listen
前,将匿名函数赋值给变量 messageHandlerXf
, 因为 remove
是根据函数名来移除事件处理函数的。
之后用 listen
添加 messageHandlerXf
作为小夫的 message
事件处理函数。
看到注释,我们知道,小夫开始放鸽子,不关心消息了。好,这里就调用 remove
来移除 message
事件的处理函数 messageHandlerXf
.
大雄到了,触发 arrive
事件,给他俩发消息。
看运行结果可知,remove
成功。大雄塞翁失马,焉知非福。
总结
大雄的故事告诉我们,如何用 JavaScript 添加自定义事件。
这里实际上用到了经典的观察者模式,也叫做发布-订阅模式。顾名思义,观察者模式,当被观察者产生变化,观察者可以知道;发布订阅模式,发布者发布消息,订阅者可收到通知。
这个时候,发布代码与订阅代码的耦合性是很低的,十分利于代码的维护。