# whatwg: https://dom.spec.whatwg.org/#interface-eventtarget
# github: https://github.com/mysticatea/[email protected]
同步发布于、知乎、微信公众号
关注微信公众号:Github星探 了解更多宝藏库
EventTarget回顾
EventTarget
是一个由可以接收事件的对象实现的接口,并且可以为它们创建监听器。
Element
,document
和window
是最常见的事件目标,但是其他对象也可以是事件目标,比如XMLHttpRequest
,AudioNode
,AudioContext
等等。
许多事件目标(包括元素,文档和window)还支持通过on...
属性和属性设置事件处理程序[1]。
Overview
target = new EventTarget();
/**
* @param {string} type 表示监听事件类型的字符串
* @param {Function|Object} listener 回调函数 或者 null or EventListener对象
* @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] 可选 这个监听事件的可选参数对象
**/
target.addEventListener(type, listener [, options])
target.removeEventListener(type, listener [, options])
target.dispatchEvent(event)
Usage
let eventTarget = new EventTarget();
let event = new Event("customEvent");
// 添加一个事件侦听器
eventTarget.addEventListener(event.type, listener)
// 添加一个只响应一次的侦听器
eventTarget.addEventListener(event.type, listenerOnce, {once: true})
// 添加一个带有handleEvent方法的对象 [2]
let handleEventObj = {
handleEvent: function(evt) {
console.log(this.desc+"\""+ evt.type +"\""); //The event type is "customEvent"
},
desc: "The event's type is"
}
eventTarget.addEventListener(event.type, handleEventObj)
// 触发这个事件的侦听器列表的所有函数
eventTarget.dispatchEvent(event)
eventTarget.dispatchEvent(event) // 再次触发
function listener(){
console.log("I am a customEvent listener")
}
function listenerOnce(){
console.log("I will only be called once")
}
EventListener对象和
handleEvent
技巧使用介绍: addEventListener, handleEvent and passing objects[2]
Implementation
通常来说,开发人员不希望观察到事件监听器的存在,事件监听器的影响仅取决于回调。
JavaScript 中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量 [引用自3]。
无论是ES modules版本还是Common JS版本,都是基于模块模式开发,因此只需要将用于存储监听器的集合放在EventTarget
构造函数之外,以达到私有化操作。该库使用WeakMap
进行存储指定监听事件的集合。
WeakMap
对对象的引用在JavaScript中具有很强的支持力,这意味着只要您具有对对象的引用,就不会对其进行垃圾回收。目前,WeakMap
和WeakSet
是JavaScript中弱引用对象的唯一方法:WeakMap
或WeakSet
将对象作为键使用不会阻止其进行垃圾回收 [4]。
创建EventTarget函数
// 创建一个存储监听事件的集合,
// 对实例化后的eventTarget对象弱引用,防止内存泄漏
let listenersMap = new WeakMap();
function EventTarget(){
listenersMap.set(this, new Map());
}
/**
* 辅助函数:读取listeners所有的监听事件映射表
* @param {EventTarget} eventTarget 指定的eventTarget引用上下文
* @returns {Map} 映射表,键为自定义事件名称,值为该事件的所有回调函数和配置属性的单向链表
* @private 私有方法
*/
function getListeners(eventTarget) {
const listeners = listenersMap.get(eventTarget);
if( listeners == null ) {
throw new TypeError(
"'this' is expected an EventTarget object, but got another value."
)
}
return listeners
}
addEventListener
为这个Event Target添加一个监听事件
- listener(
null
or an EventListener Object),外加拓展一个Function
类型
测试用例:if(listener == null) { return } if(typeof listener !== "function" && typeof listener !== "object") { throw new TypeError("Failed to execute 'addEventListener' on 'EventTarget': The callback provided as parameter 2 is not an object.") }
- options 监听事件type设定属性。为兼容起见,可以是boolean类型或者object类型。当它为boolean类型时,和设置
capture
属性时的行为是一样的;- 当设置为
true
或{ capture:true }
时,可以防止事件的eventPhase
属性值为BUBBLING_PHASE
时调用listener回调函数 - 当设置为
false
或{ capture:false }
(或不设置,默认为false)时,可以防止事件的eventPhase
属性值为CAPTURING_PHASE
时调用listener回调函数 - 无论哪种方式,如果事件的
eventPhase
属性值为AT_TARGET
时都会调用listener回调函数 - 当设置属性
{passive:true}
时,即使调用preventDefault()
事件也不会取消对回调函数的触发 - 当设置属性
{once:true}
时,表示回调函数仅能被调用一次,之后该type类型的事件会被删除,即再次dispatchEvent
时也不会被执行
- 当设置为
这里重点讲解EventTarget,所以Event相关属性暂不详细介绍 [详见5]
EventTarget.prototype.addEventListener = function(type, listener, options) {
let optionsIsObj = Boolean(options !== null && typeof options === "object");
const capture = optionsIsObj ? Boolean(options.capture) : Boolean(options);
const passive = optionsIsObj && Boolean(options.passive);
const once = optionsIsObj && Boolean(options.once);
const newNode = {
listener,
listenerType: capture ? CAPTURE : BUBBLE, // 事件在捕获阶段触发还是冒泡阶段触发
passive,
once,
next:null, // 用于链表的链接点,为null表示链表的尾部
}
// 获取当前eventTarget对象的所有事件的集合
const listeners = getListeners(this);
let node = listeners.get(type); // 指定type事件的链表
// 如果第一个节点为空,则将其设置为第一个节点
if(node === undefined) {
listeners.set(type, newNode);
return
}
// 遍历整个链表至尾部,用以检查重复项
let prev = null;
while(node !== null) {
if(
node.listener === listener &&
node.listenerType === listenerType
) {
// 忽略重复并结束之后的所有操作
return
}
prev = node; //赋值前一个节点的引用
node = node.next;
}
// 否则,找到最后一项,将next赋给newNode,建立链接
prev.next = newNode;
}
removeEventListener
移除这个Event Target的事件监听器列表中具有相同类型、回调(同一引用)和选项(只对比capture属性)的事件监听器
EventTarget.prototype.removeEventListener = function(type, listener, options){
if(listener == null) {
return
}
const listeners = getListeners(this);
const capture = isObject(options)
? Boolean(options.capture)
: Boolean(options);
const listenerType = capture ? CAPTURE : BUBBLE;
let prev = null;
let node = listeners.get(eventName);
while (node != null) {
if (
node.listener === listener &&
node.listenerType === listenerType
) {
// 如果不是第一个,则将当前结点的前一个和后一个相连
// 从而使其失去引用
if (prev !== null) { //
prev.next = node.next;
// 如果是第一个
} else if (node.next !== null) {
listeners.set(eventName, node.next);
} else {
listeners.delete(eventName);
}
return
}
prev = node;
node = node.next;
}
}
dispatchEvent
向一个指定的事件目标派发一个事件(Event实例化对象),并以合适的顺序同步调用目标元素相关的事件处理函数。
EventTarget.prototype.dispatchEvent = function(event){
if (event == null || typeof event.type !== "string") {
throw new TypeError('"event.type" should be a string.')
}
const listeners = getListeners(this);
const eventName = event.type;
let node = listeners.get(eventName);
if (node == null) {
return true
}
let prev = null;
while (node != null) {
// Remove this listener if it's once
if (node.once) {
if (prev !== null) {
prev.next = node.next;
} else if (node.next !== null) {
listeners.set(eventName, node.next);
} else {
listeners.delete(eventName);
}
} else {
prev = node;
}
// Call this listener
if (typeof node.listener === "function") {
try {
node.listener.call(this, event);
} catch (err) {
if (typeof console !== "undefined" && typeof console.error === "function") {
console.error(err);
}
}
// 如果是Object,则会自动寻找一个名为`handleEvent`的属性下的函数,并将当前事件作为参数传递
} else if (
node.listenerType !== ATTRIBUTE &&
typeof node.listener.handleEvent === "function"
) {
node.listener.handleEvent(event);
}
}
基本实现了基于WHATWG标准的EventTarget,再来一点小小的拓展,锦上添花,毕竟定位上这是一个shim库嘛
什么是shim?
在计算机编程中,shim(中文:垫片)是一个小型库,可以透明地拦截API,更改传递的参数,处理操作本身或将操作重定向到其他地方[6]。
Shims可用于在较新的环境中支持旧API,或在较旧的环境中支持新的API。
EventTarget API最早出现在浏览器(browser)环境,随着Node.js在日常开发中的权重越来越高,为了使web开发人员能平滑到node环境中,就需要大量这种shim库做兼容互通。比如服务端开发、跨平台开发React Native和Weex。还有最近很火的Deno,迷渡(justjavac)大大曾将 Deno 的 WHATWG 规范兼容性从不足 40% 提升到如今的 70%。
polyfill(中文:腻子) 属于shim的子集,注重点更多的在在较旧的环境中支持新的API(老版本兼容),比如兼容IE 8版本以下的array.map
方法,最简单的方式就是:if (!Array.prototype.map){Array.prototype.map = function(){/.../}}
Extensions
design: on${type}
class SubClass extends EventTarget(type:?EventType){/.../}
// extensions type listener use by on${type}
new SubClass().on${type} = (e)=>{/.../}
#EventTarget
- 如果没有参数,则为构造函数
- 如果有参数,此函数将返回CustomEventTarget构造函数。
- 比如:
- class A extends EventTarget{}
- class B extends EventTarget("message"){}
- class C extends EventTarget("message", "error"){}
- class D extends EventTarget(["message", "error"]){}
function EventTarget(){
/* eslint-disabled consistent-return*/ //临时禁用"使用一致的return语句"规则
if(this instanceof EventTarget) { // When use `new EventTarget()`, return end
return
}
/**
* When a function is called in the global scope, the `this` object points to `window`[7]
* Like `EventTarget()`, `class SubClass extends EventTarget(){}`
*/
if(arguments.length===1&&Array.isArray(arguments)){// class D
return defineCustomEventTarget(arguments)
}
if(arguments.length > 0){ // class B/class C
const types = new Array(arguments.length);
for(let i=0;i
函数内部的另一个特殊对象是 this,其行为与 Java 和 C#中的 this 大致类似。换句话说,this引用的是函数据以执行的环境对象。
在全局范围内调用函数时,“ this”对象指向“ window”。当实例化函数(也就是new操作)时,"this"指向这个函数[7]
#defineCustomEventTarget
篇幅有限,拓展功能的代码就不一一赘述,步骤可分为以下两点:
- 将传入的参数依次加工成
on${type}
并存储到原型链中,默认值为null - 为
on${type}
定义函数时,通过setter
拦截和验证定义的值,将符合规范的值以类似addEventListener(type, value)
的方式添加到listenersMap集合中
使用案例
React-Native同构WebSocket的JS层代码
class WebSocket extends EventTarget(["open", "message", "close", "error"]){
constructor(props){
this.url = props.url;
// 监听来自Native端的事件
NativeEventEmitter.addEventListener('websocketMessage', ev => {
this.dispatchEvent(new WebSocketEvent('message', {data:ev.data}))
})
}
}
// 使用
let ws = new WebSocket("ws://host")
ws.onopen = (e) =>{/.../};
// onopen相当于替代了以下代码
ws.addEventListener("open",function(){
/.../
})
ws.onmessage = (e) =>{/.../};
ws.onclose = (e) =>{/.../}
ws.onerror = (e) =>{/.../}
Conclusion
- 从实现
addEventListener
方法可以得知,回调函数的对比是通过引用对比,只有在重复添加相同事件类型(CAPTURE
和BUBBLE
)和同一引用的回调函数时才会被忽略,其中一旦相同事件类型的回调函数不是指向同一引用时,则会重复添加到listener队列中。同理removeEventListener
删除指定事件监听也是通过引用对比 -
addEventListener
除了能添加函数之外,还能添加一个带有handleEvent
属性的对象 - 如果需要为构造函数创建一个私有变量,可以考虑使用
WeakMap
。此外虽然标准说事件集合不可见,但chrome在调试工具上还是开放了可见方法getEventListeners
(仅针对DOM)[9] - 利用
instanceof
就可以非常灵活地判断函数是否是全局调用还是被实例化。
Reference
- [1] EventTarget - Web API接口参考 | MDN
- [2] addEventListener, handleEvent and passing objects [J] Ryan Seddon
- [3] JavaScript高级程序设计(第3版中译)[M] p.186 私有变量
- [4] 弱引用与垃圾回收 | v8.dev
- [5] ⚠️event-shim 暂无敬请期待
- [6] https://en.wikipedia.org/wiki/Shim_(computing)
- [7] JavaScript高级程序设计(第3版中译)[M] p.114
- [8] https://developers.google.com/web/tools/chrome-devtools/console/utilities#geteventlisteners
- [9] 其他:
{passive:true}
使用被动事件侦听器提高滚动性能