所有的js库或框架都提供快捷DOM事件绑定方法,比如JQuery中:

$("#target").click((() => console.log("clicked"));

以及angular中的

ng-click="click(xxx)"

但这背后是什么,有些了解,但比较模糊,今天就详细了解一下。

原生js通常有2种方法给DOM添加事件: on-event handler 以及 EventTarget.addEventListener()

on-event handler

这是比较古老的一种方式,可以直接在HTML元素上添加on-event处理,比如onclick;也可以通过js添加,参见下面的例子:

注意,这两种方式的效果是不完全相同的,具体表现在this的值上:直接写下在HTML元素上的方法,this指向的是它注册的那个DOM元素;而通过js添加的情况,this在非严格模式下指向的是window,严格模式下是undefined

EventTarget.addEventListener()

on-event方法一个事件只能添加一个handler,当然,可以在这个handler里再调用别的handler来达到注册多个handler的效果;EventTarget.addEventListener()是更现代的方法,可以添加多个handler(listener), JQuery使用的就是这种方法:

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);

其中,

  • type: 表示事件类型,详细的事件类型可以参考这里,这里记录几种常见的event:
    • DOMContentLoaded: 原始HTML文档载入完成,这个时候stylesheets, images, subframes可能还没有完成,但对于scripts,情况有些复杂,简单来说,在该事件发生中,同步脚本已经加载并执行完,但异步脚本可能还没有。
    • load: 所有资源以及它们的依赖资源都完成加载,所以通常情况下,load事件要晚于DOMContentLoaded
    • focus, blur
    • resize, scroll
    • keydown, keyup, keypress
    • mouseenter, mouseover, mousemove, mousedown, mouseup, click, dblclick, wheel, mouseleave, mouseout
  • listener: 可以是js方法或者实现了Event接口的对象
  • options: 可以有以下几个属性
    • capture: boolean, 详见下面的useCapture
    • once: boolean, 表示该事件是不是只被触发一次,如果设成true,那么被触发后,listener将被移除
    • passive: boolean, 表示该listener会不会调用preventDefault(); preventDefault()表示阻止浏览器展示控件的默认行为,比如checkbox对于click的默认行为是选中或取消选中,如果在选中事件中调用了preventDefault(),那么控件将不会被选中;可以用来阻止用户进行非法操作;passive还有一个作用,就是提高页面滚动的性能,详细请看这里
  • useCapture: useCapture表示是不是需要在Capture Phase捕获事件,关于Capture Phase详见后面的解释,默认为false. W3C标准中event触发有3个阶段:要理解useCapture的含义,先要理解事件发生的3个阶段:
    • Capture Phase: 自顶向下,即从window到event target,查看路径上的每个元素是否定义了handler;
    • Target Phase: 又叫on-target phase,即自顶向下找到event target时;
    • Bubble Phase: 自底向上,即从event target到window,查看路径上的每个元素是否定义了handler. 为什么要分3个阶段呢?这要得从浏览器的历史说起,简单来说,在很久以前,世上只有2个浏览器的时候,其中一个采用的是capture模式,即事件是自顶向下传递的;而另一个采用的是bubble模式,事件从当前的target开始向下冒泡;而W3C为了兼容这2种模式,就采用了折衷的办法,定义了事件的3个阶段。

默认情况下浏览器采用的是bubble模式,useCapture是false,请看下面的例子:

不难猜中结果:孩子结点的click先触发,然后冒泡到父结点,触发父结点的click事件。 可以把useCapture改成true看看结果是什么。

这里再多说一句,如果我们只想触发孩子结点的click事件,而不想冒泡到父结点怎么办?用event.stopPropagation(), event.stopPropagation()将阻止事件冒泡(但Capture Phase还是会发生)。

参考资料