EventTarget の継承可能化による EventEmitter の代替
Intro
念願 だった EventTarget の constructible/subclassable が DOM の仕様にマージされた。
これにより、いわゆる EventEmitter のブラウザ移植が不要になることが期待される。
Update
Chrome Canary 64 で実装が確認できたため、DEMO を追加した。
EventTarget
EventTarget には addEventListener
, removeEventListener
, dispatchEvent
が定義されている。
これは、ブラウザが内部で生成する Event や、任意に生成された CustomEvent を発火/補足するために利用される。
callback = console.log.bind(console)
$div = document.createElement('div')
$div.addEventListener('foo', callback)
$div.dispatchEvent(new CustomEvent('foo', {detail:'bar'}))
// CustomEvent {type: "foo", detail: 'bar'...
$div.removeEventListener('foo', callback)
この場合、$div
は Element < Node < EventTarget
と、祖先に EventTarget を持っている。
同様に EventTarget を祖先に持つ要素では、このパターンのハンドリングが可能であるが、任意のクラスを EventTarget にすることができなかった。
EventEmitter
Node では EventEmitter が、メソッド名は違えど同等の役割を果たしていると言える。
例えば process
は EventEmitter を継承している。
callback = console.log.bind(console)
process.on('foo', callback)
process.emit('foo', 'bar')
// bar
process.removeListener('foo', callback)
大きな違いは、EventEmitter が任意のクラスで継承できる点だ。
したがって、非同期処理をクラスに閉じ込め、加工したイベントとして外に公開するといった設計が可能になる。
以下は setInterval
を抽象化したタイマの例だ。
EventEmitter = require('events')
class Timer extends EventEmitter {
constructor(interval) {
super()
setInterval(() => {
this.emit('tick', 'tick')
}, interval)
}
}
timer = new Timer(100)
timer.on('tick', console.log.bind(console))
またこうしたイベントの抽象化の先に stream がある。
EventEmitter porting
これまでは、ブラウザ上で任意の class を EventTarget にすることができなかった。
そこで、Node における EventEmitter を用いた設計と同等のことを行うためには、EventEmitter のポーティングなどが利用されていた。
例えば browserify は https://github.com/Gozala/events を使っており、筆者も 同じようなこと をしたことがある。
しかし、こうした汎用的な処理をより効率よく実現するために、EventTarget が継承可能となる仕様が追加された。
実装されれば、メソッド名をすり合わせる目的以外で EventEmitter porting は不要となるだろう。
constructible/subclassable EventTarget
具体的には以下のようなコードが書けるようになる。
class Timer extends EventTarget {
constructor(interval) {
super()
setInterval(() => {
this.dispatchEvent(new CustomEvent('tick'))
}, interval)
}
}
timer = new Timer(100)
timer.addEventListener('tick', console.log.bind(console))
EventTarget を元に EventEmitter とメソッド名をすり合わせた shim を書く場合は以下のような感じだろうか。
class EventEmitter extends EventTarget {
constructor() {
// snip
}
on(type, listener) {
this.addEventListener(type, listener)
}
emit(type, val) {
this.dispatchEvent(new CustomEvent(type, {detail: val}));
}
// ... and more
}
もしくは、多くの要素が EventTarget を継承していることを利用して、以下のようなこともできる。
EventTarget.prototype.on = EventTarget.prototype.addEventListener
EventTarget.prototype.off = EventTarget.prototype.removeEventListener
EventTarget.prototype.emit = function(name, detail) {
this.dispatchEvent(new CustomEvent(name, {detail}))
}
これで、例えば Button 要素にも on
などが生える。
document.querySelector('button').on('click', (e) => {
console.log('click')
})
ただし、EventEmitter は EventTarget よりも機能が多く、例えば listeners()
や eventNames()
などは、EventTarget への移譲だけでは実装できない。
それらが必要な場合は、別途イベントとリスナの管理が必要になるだろう。こうした機能が必要な場合は、要するに EventEmitter そのものを必要としてるということなので、porting は依然必要になる。
しかし、EventTarget 相当を実現するためだけに EventEmitter を導入していた場合は、EventTarget が継承できるだけで十分な場合も少なくはないだろう。
その場合はネイティブの実装だけで足りるようになるため、実装が進むことに期待したい。
DEMO
動作するデモを以下に用意した。