WHATWG标准EventTarget接口的实现,外加很少的扩展 - event-target-shim细节分析

# whatwg: https://dom.spec.whatwg.org/#interface-eventtarget
# github: https://github.com/mysticatea/[email protected]
同步发布于、知乎、微信公众号
关注微信公众号:Github星探 了解更多宝藏库

EventTarget回顾

EventTarget是一个由可以接收事件的对象实现的接口,并且可以为它们创建监听器。

Elementdocumentwindow是最常见的事件目标,但是其他对象也可以是事件目标,比如XMLHttpRequestAudioNodeAudioContext等等。

许多事件目标(包括元素,文档和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中具有很强的支持力,这意味着只要您具有对对象的引用,就不会对其进行垃圾回收。目前,WeakMapWeakSet是JavaScript中弱引用对象的唯一方法:WeakMapWeakSet将对象作为键使用不会阻止其进行垃圾回收 [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添加一个监听事件

  1. 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.")
      }
    
  2. options 监听事件type设定属性。为兼容起见,可以是boolean类型或者object类型。当它为boolean类型时,和设置capture属性时的行为是一样的;
    1. 当设置为true{ capture:true }时,可以防止事件的eventPhase属性值为BUBBLING_PHASE时调用listener回调函数
    2. 当设置为false{ capture:false }(或不设置,默认为false)时,可以防止事件的eventPhase属性值为CAPTURING_PHASE时调用listener回调函数
    3. 无论哪种方式,如果事件的eventPhase属性值为AT_TARGET时都会调用listener回调函数
    4. 当设置属性{passive:true}时,即使调用preventDefault()事件也不会取消对回调函数的触发
    5. 当设置属性{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 NativeWeex。还有最近很火的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

篇幅有限,拓展功能的代码就不一一赘述,步骤可分为以下两点:

  1. 将传入的参数依次加工成on${type}并存储到原型链中,默认值为null
  2. 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

  1. 从实现addEventListener方法可以得知,回调函数的对比是通过引用对比,只有在重复添加相同事件类型CAPTUREBUBBLE)和同一引用的回调函数时才会被忽略,其中一旦相同事件类型的回调函数不是指向同一引用时,则会重复添加到listener队列中。同理 removeEventListener删除指定事件监听也是通过引用对比
  2. addEventListener除了能添加函数之外,还能添加一个带有handleEvent属性的对象
  3. 如果需要为构造函数创建一个私有变量,可以考虑使用WeakMap。此外虽然标准说事件集合不可见,但chrome在调试工具上还是开放了可见方法getEventListeners(仅针对DOM)[9]
  4. 利用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}使用被动事件侦听器提高滚动性能

你可能感兴趣的:(WHATWG标准EventTarget接口的实现,外加很少的扩展 - event-target-shim细节分析)