DOM系列:事件绑定的姿势

发布于 大漠

在上一节中,学习和了解了DOM事件模型,了解到JavaScript中每种事件模型都有其自己独具的特性。不同的事件模型中,绑定DOM事件的姿势也将略有差异,在这一节中,我们一起来学习JavaScript中DOM事件是如何绑定的。

在JavaScript中,给DOM元素绑定事件主要分为两大类:HTML中直接绑定JavaScript中绑定

HTML中直接绑定DOM事件

在HTML中绑定事件叫做内联绑定事件。其使用方式非常的简单,就是在HTML的元素中使用<event>属性来绑定事件,比如onclick这样的on(type)属性,其中type指的就是DOM的事件(比如click),它可以给这个DOM元素绑定一个类型的事件。比如,要为button元素绑定一个click事件,那么就可以像下面这样使用:

<!-- HTML -->
<button onclick="show();">Click Me</button>

<script>
    function show() {
        console.log('Show Me!')
    }
</script>

当用户在按钮上单击鼠标时,onclick中的代码将会运行,在上面的示例中,将会调用show()函数。

这种方式对应的也是DOM Level0 模型中的事件绑定方式。虽然这种方式也能正常的DOM事件绑定方式,但这种方法是非常不鼓励的。因为它是一种非常不灵活的事件绑定方式,它将HTML结构和JavaScript混合在一起。

JavaScript中绑定DOM事件

在JavaScript中绑定DOM事件有两种方法:

  • element.on(type) = listener
  • element.addEventListener(type, listener, useCapture)

而这两种方法用DOM事件模型来区分,或者划分的话,又被划分为:

  • DOM Level1
  • DOM Level2

接下来,咱们看看这两个方法的使用。

element.on(type) = listener

这种事件绑定方式和前面介绍的,在HTML中使用onclick=listener绑定DOM事件有点类似,不同的是,前者在HTML中绑定,而这种方式是从HTML中分离出来。咱们将上面的示例改一下,就会像下面这样:

<!-- HTML -->
<button>Click Me!</button>

// JavaScript
function show() {
    console.log("Show Me!");
}

let btn = document.querySelector('button')
btn.onclick = show;

这个时候你用鼠标点击按钮时,同样在控制台中能看到像下图这样的信息:

这里有一个细节需要注意,在onclick调用事件是,应该是show,而不是show()。如果使用的是btn.onclick=show(),那么show()将是函数执行的结果,因此最后一个代码中的onclick就没有定义(函数什么也没有返回)。这样是行不通的。

但我们在HTML中这样调用是可以执行的,前面的示例也向大家演示了。如果你实在想调用show()函数,那么前面的示例,你可以修改成这样:

function show() {
    console.log("Show Me!");
}

let btn = document.querySelector('button')
btn.onclick = function () {
    show()
};

细想一下,其实这和在HTML中内联绑定函数是一样的,同样是给DOM的元素onclick属性赋值一个函数,而他们的区别是:函数中的this指向当前元素(内联),而后面这种方式是在JavaScript中做的。另外一个区别就是内联方式赋值的是一段JavaScript字符串,而这里赋值的是一个函数,它可以接受以一个参数event,这个参数是点击的事件对象。

<!-- HTML -->
<button onclick="show(this)">DOM Level0:Click Me!</button>

<button id="btn">DOM Level1:Click Me!</button>

// JavaScript
function show(e) {
    console.log(e)
}

let btn = document.getElementById('btn')

btn.onclick = show;

比如,上面的示例,你分别点击两个按钮,在控制台上输入的结果将会是像下图这样:

另外,用赋值绑定函数也有一个缺点,那就是它只能绑定一次

<!-- HTML -->
<button onclick="show(this);show(this);">DOM Level0:Click Me</button>
  <button id="btn">DOM Level1:Click Me</button>

// JavaScript
function show(e) {
    console.log(e);
}

let btn = document.getElementById('btn')
btn.onclick = show;
btn.onclick = show;

这个时候点击按钮的结果如下:

这种方法将我们的JavaScript和HTML分开。而且这种方式具有其自己的特征:

  • 它的本质就是给HTML元素添加相应的属性
  • 它的事件处理程序(绑定的事件)在执行时,其中的this指向当前的元素
  • 该方式不会做同一元素的同类型事件绑定累加。也就是当你在同一个元素上多次绑定相同类型的监听函数时,后者会覆盖前者

element.addEventListener(type, listener, useCapture)

在编写DOM脚本时能最大程度的控制事件,我们希望使用DOM Level2事件监听器。它的语法是这样子的:

element.addEventListener(type, listener[, useCapture]);

具体意思是:

  • element:表示要监听事件的目标对象,可以是一个文档上的元素Document本身,window或者XMLHttpRequest
  • type:表示事件类型的字符串,比如clickchangetouchstart
  • listener:当指定的事件类型发生时被对知到的一个对象。该参数必是实现EventListener接口的一个对象或函数,比如前面示例中的show()函数
  • useCapture:设置事件的捕获或者冒泡,它有两个值,其中true表示事件捕获,为false是事件冒泡,默认值为false

我们可以使用element.addEventListener(type, listener[, useCapture]);来修改前面的示例:

<!-- HTML -->
<button>Click Me!</button>

// JavaScript
function show () {
    console.log("Show Me!")
}

let btn = document.querySelector('button')

btn.addEventListener('click', show, false)

这个时候点击按钮,浏览器控制台输出的结果如下图所示:

这种DOM事件绑定的方式看起来比前面的方法要复杂一些,事实上这种复杂也就是额外的花了一些时间输入代码。addEventListener给DOM元素绑定事件的一大优势在于可以根据需要为事件提供尽可能多的处理程序(监听函数)。你还可以指定在事件捕获或事件冒泡(addEventListener中的第三个参数)。

另外,在使用addEventListener给DOM绑定事件时,其中第二个参数,即监听函数,这个函数中的this指向当前的DOM元素,同样,函数也接受一个event参数。比如上面的示例,咱们修改之后:

<!-- HTML -->
<button id="btn">Click Me</button>

// JavaScript
function show (e) {
    console.log(this)
    console.log(e)
}

let btn = document.getElementById('btn')

btn.addEventListener('click', show, false)

这个时候,你在浏览器中点击按钮,浏览器控制器将会输出的结果像下面这样:

使用addEventListener来给DOM元素绑定事件,还有一个优势,它可以给同一个DOM元素绑定多个函数,比如:

<!-- HTML -->
<button id="btn">Click Me!</button>

// JavaScript
function foo () {
    console.log('Show foo function')
}

function bar () {
    console.log('Show bar function')
}

let btn = document.getElementById('btn')

btn.addEventListener('click', foo)
btn.addEventListener('click', bar)

这个时候,点击按钮,浏览器控制台输出的结果像下面这样:

从结果中我们可以看出,给btn元素绑定的click事件,两个函数都被执行了,并且执行顺序按照绑定的顺序执行。

前面也提到过,addEventListener的第三个参数,如果给其添加第三个参数时,来看看有何不同。咱们在上面的示例中稍作修改:

<!-- HTML -->
<button id="btn">Click Me</button>

// JavaScript

function foo () {
    console.log("Show foo function")
}

function bar () {
    console.log("Show bar function")
}

let btn = document.getElementById('btn')

// foo: true; bar: true
console.log('==== foo: true; bar: true ====')
btn.addEventListener('click', foo, true) // => Show foo function
btn.addEventListener('click', bar, true) // => Show bar function

// foo: true; bar: false
console.log('==== foo: true; bar: false ====')
btn.addEventListener('click', foo, true)  // => Show foo function
btn.addEventListener('click', bar, false) // => Show bar function

// foo: false; bar: true
console.log('==== foo: false; bar: true ====')
btn.addEventListener('click', foo, false) // => Show foo function
btn.addEventListener('click', bar, true)  // => Show bar function

// foo: false; bar: false
console.log('==== foo: false; bar: false ====')
btn.addEventListener('click', foo, false) // => Show foo function
btn.addEventListener('click', bar, false) // => Show bar function

从浏览器控制台输出的结果可以看出,不管useCapture设置的是true还是flase,对于输出的结果都是一样的。这也说明,使用addEventListener绑定的事件执行顺序只和绑定顺序有关,和useCapture并无关

也就是说:

使用addEventListener可以给同一个DOM元素绑定多个函数,并且它的执行顺序将按照绑定的顺序执行!

上面我们看到的是给同一个DOM元素绑定的是不同的监听函数,如果我们给同一个DOM元素多次绑定同一个函数:

<!-- HTML -->
<button id="btn">Click Me!</button>

// JavaScript
function show () {
    console.log(this)
}

let btn = document.getElementById('btn')

btn.addEventListener('click', show)
btn.addEventListener('click', show)

输出的结果如下:

从结果上可以看出,我们虽然在同一个DOM元素上绑定了两次click事件,而且监听函数都是show,但我们输出的结果却只有一个,如上图所示。这个时候addEventListener的第三个参数,都是默认值false

我们再把addEventListener修改一下:

btn.addEventListener('click', show, true)
btn.addEventListener('click', show, false)

输出的结果如下:

当你点击按钮时,show()函数被执行了两次。这个时候addEventListener的第三个参数分别是truefalse。再测试一下,如果第三个参数都是true呢?结果如下:

和同样为false一样,虽然绑定了两次,但只输出一个结果。最后再看一种情形:

btn.addEventListener('click', show, false)
btn.addEventListener('click', show, true)

输出的结果也是两次。通过这几个简单的示列,我们可以得到的结果是:

使用addEventListener可以给一个DOM元素绑定同一个函数,最多只能绑定useCapture类型不同的两次!

简单的归纳一下addEventListener给DOM元素绑定事件的一些特征:

  • 当用户进行一个操作时,浏览器会根据该操作依次触发相应的事件监听函数,此时我们暂时不会考虑你程序中的人为阻止,比如stopPropagationpreventDefault等操作
  • 当同一个元素上绑定了多次同类型事件,比如button元素上做了多次click事件,那么遵循“先绑定先触发”的原则,而且最多只能绑定useCapture类型不同的两次
  • 它的事件处理程序在执行时,其中this指向当前的元素

事件对象

从前面的示例中,我们可以看到,DOM事件调用处理程序时,可以给这个处理程序传一个参数,比如event参数。事实上,当事件发生时,浏览器会创建一个事件对象,将详细信息放入这个对象当中,并将其作为参数传递给处理程序。比如下面这个示例:

<!-- HTML -->
<button id="btn">Click Me!</button>

// JavaScript
function show(event) {
    console.log(event)
}

let btn = document.getElementById('btn')

btn.addEventListener('click', show)

当你在浏览器中点击按钮时,浏览器控制台中就会输出这个Event对象的所有信息,如下图所示:

Event对象在event第一次触发的时候被创建出来,并且一直伴随着事件在DOM结构中流转的整个生命周期。event对象会被作为第一个参数传递给事件监听的回调函数。我们可以通过这个event对象来获取到大量当前事件相关的信息:

  • type (String):事件的名称
  • target (node):事件起源的DOM节点
  • currentTarget?(node):当前回调函数被触发的DOM节点(后面会做比较详细的介绍)
  • bubbles (boolean):指明这个事件是否是一个冒泡事件(接下来会做解释)
  • preventDefault(function):这个方法将阻止浏览器中用户代理对当前事件的相关默认行为被触发。比如阻止<a>元素的click事件加载一个新的页面
  • stopPropagation (function):这个方法将阻止当前事件链上后面的元素的回调函数被触发,当前节点上针对此事件的其他回调函数依然会被触发。(我们稍后会详细介绍。)
  • stopImmediatePropagation (function):这个方法将阻止当前事件链上所有的回调函数被触发,也包括当前节点上针对此事件已绑定的其他回调函数。
  • cancelable (boolean):这个变量指明这个事件的默认行为是否可以通过调用event.preventDefault来阻止。也就是说,只有cancelabletrue的时候,调用event.preventDefault才能生效。
  • defaultPrevented (boolean):这个状态变量表明当前事件对象的preventDefault方法是否被调用过
  • isTrusted (boolean):如果一个事件是由设备本身(如浏览器)触发的,而不是通过JavaScript模拟合成的,那个这个事件被称为可信任的(trusted)
  • eventPhase (number):这个数字变量表示当前这个事件所处的阶段(phase):none(0), capture(1),target(2),bubbling(3)。我们会在下一个部分介绍事件的各个阶段
  • timestamp (number):事件发生的时间

此外事件对象还可能拥有很多其他的属性,但是他们都是针对特定的event的。比如,鼠标事件包含clientXclientY属性来表明鼠标在当前视窗的位置。比如像下面这个示例:

<!-- HTML -->
<button id="btn">Click Me!</button>

// JavaScript
function show(event) {
    console.log(`${event.type} at ${event.currentTarget}`)
    console.log(`Coordinates: (${event.clientX}, ${event.clientY})`)
}

let btn = document.getElementById('btn')

btn.addEventListener('click', show)

当你在button不同的位置点击按钮时,浏览器控制台将输出不同的结果,特别是鼠标坐标:

对象处理程序:handleEvent

我们可以使用addEventListener将对象指定为事件处理程序。当一个事件发生时,它的handleEvent方法也随之被调用。

比如,将上面的示例做一下修改:

btn.addEventListener('click', {
    handleEvent(event) {
        console.log(`${event.type} at ${event.currentTarget}`)
        console.log(`Coordinates: (${event.clientX}, ${event.clientY})`)
    }
})

这里输出的结果和上面示例输出的结果是相似的:

换句话说,当addEventListener接收到一个对象作为处理程序时,它在发生事件时调用object. handleevent (event)

我们也可以用一个类:

class Show {
    handleEvent(event) {
        switch(event.type) {
            case 'mousedown':
                btn.innerHTML = 'Mouse button pressed';
                break;
            case 'mouseup':
                btn.innerHTML = '...and released.';
                break
        }
    }
}

let show = new Show();
btn.addEventListener('mousedown', show)
btn.addEventListener('mouseup', show)

当你在按钮上按下鼠标和松开鼠标时,按钮的文本内容将会变化:

在这里,同一个对象处理这两个事件。请注意,我们需要显式地设置事件来使用addEventListener监听。Show对象只在这里得到musedownmouseup,而不是任何其他类型的事件。

handleEvent方法不需要单独完成所有工作。它可以调用其他特定于事件的方法,像下面这样:

class Show {
    handleEvent(event) {
        // mousedown -> onMousedown
        let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
        this[method](event);
    }

    onMousedown() {
        btn.innerHTML = "Mouse button pressed";
    }

    onMouseup() {
        btn.innerHTML += "...and released.";
    }
}

let show = new Show();
btn.addEventListener('mousedown', show);
btn.addEventListener('mouseup', show);

现在事件处理程序显然是分开的,这可能更容易支持。

事件阶段

通过上面的学习,我们知道在JavaScript中怎么给DOM元素绑定事件。而且在DOM Level2的DOM模型中,给DOM元素绑定事件会经历三个阶段:捕获阶段处于目标阶段冒泡阶段。而addEventListener的第三个参数useCapture就是用来指定该事件监听函数是捕获阶段还是冒泡阶段被触发。

所以我们现在要了解一些概念,了解它们是如何运作?

简而言之:事件一开始从文档的根节点流向目标对象(捕获阶段),然后在目标对向上被触发(目标阶段),之后再回溯到文档的根节点(冒泡阶段)。

事件阶段中的事件捕获阶段目标阶段事件冒泡阶段是整个事件流的三个重要概念,也是理解JavaScript中DOM事件的重要概念,在这里只先简单的阐述其概念,有关于更细的介绍,将在此系列的后续文章中阐述。

事件捕获阶段

事件的第一个阶段是捕获阶段。事件从文档的根节点出发,随着DOM树的结构向事件的目标节点流去。途中经过各个层次的DOM节点,并在各节点上触发捕获事件,直到到达事件的目标节点。捕获阶段的主要任务是建立传播路径,在冒泡阶段,事件会通过这个路径回溯到文档跟节点。

我们可以通过将addEventListener的第三个参数设置成true来为事件的捕获阶段添加监听回调函数。在实际应用中,我们并没有太多使用捕获阶段监听的用例,但是通过在捕获阶段对事件的处理,我们可以阻止类似click事件在某个特定元素上被触发。

btn.addEventListener('click', function(event) {
    event.stopPropagation();
}, true);

如果你对这种用法不是很了解的话,最好还是将useCapture设置为false或者undefined,从而在冒泡阶段对事件进行监听。

目标阶段

当事件到达目标节点的,事件就进入了目标阶段。事件在目标节点上被触发,然后会逆向回流,直到传播至最外层的文档节点。

对于多层嵌套的节点,鼠标和指针事件经常会被定位到最里层的元素上。假设,你在一个<div>元素上设置了click事件的监听函数,而用户点击在了这个<div>元素内部的<p>元素上,那么<p>元素就是这个事件的目标元素。事件冒泡让我们可以在这个<div>(或者更上层的)元素上监听click事件,并且事件传播过程中触发回调函数。

冒泡阶段

事件在目标元素上触发后,并不在这个元素上终止。它会随着DOM树一层层向上冒泡,直到到达最外层的根节点。也就是说,同一个事件会依次在目标节点的父节点,父节点的父节点。。。直到最外层的节点上被触发。

将DOM结构想象成一个洋葱,事件目标是这个洋葱的中心。在捕获阶段,事件从最外层钻入洋葱,穿过途径的每一层。在到达中心后,事件被触发(目标阶段)。然后事件开始回溯,再次经过每一层返回(冒泡阶段)。当到达洋葱表面的时候,这次旅程就结束了。

冒泡过程非常有用。它将我们从对特定元素的事件监听中释放出来,相反,我们可以监听DOM树上更上层的元素,等待事件冒泡的到达。如果没有事件冒泡,在某些情况下,我们需要监听很多不同的元素来确保捕获到想要的事件。

绝大多数事件会冒泡,但并非所有的。当你发现有些事件不冒泡的时候,它肯定是有原因的。不相信?你可以查看一下相应的规范说明

该使用哪种方式

上面介绍了三种方式来给 DOM元素绑定事件。但你不应该使用HTML事件处理程序属性,因为这些属性已经过时了,而且也是不好的做法,前面有介绍过。

另外两种是相对可互换的,到少对于简单的用途:

  • 事件处理程序属性功能和选会更少,但是具有更好的跨浏览器兼容性
  • DOM Level2 事件(addEventListener)更强大,但也可以变得更加复杂,并且支持不足(IE9以下不支持)。

addEventListener的主要优点是,如果需要的话,可以使用removeEventListener删除事件处理程序代码,而且如果有需要,可以向同一类型的元素添加多个监听器。例如,你可以在一个元素上多次调用addEventListener('click', function() { ... }),并可在第二个参数中指定不同的函数。对于事件处理程序属性来说,这是不可能的,因为后面任何设置的属性都会尝试覆盖较早的属性,例如:

element.onclick = function1;
element.onclick = function2;

总结

在JavaScript中给DOM绑定事件有三种姿势:

  • HTML属性:onclick="..."
  • DOM属性:element.onclick = function
  • 方法:element.addEventListener(type, listener[, useCapture]);绑定事件;removeEventListener删除事件

HTML属性很少使用,因为HTML标记中使用JavaScript代码,看起来有点怪,而且耦合在一起,也不能写很多代码。

DOM属性可以使用,但是我们不能为特定事件分配多个处理程序(事件监听函数)。在许多情况下,这处限制并不紧迫。

最后一种方式是最灵活的,但也是最长的书写方式。很少有事件只适用于它,比如transtionedDOMContentLoadedaddEventListener还支持对象作为事件处理程序。在这种情况下,在事件发生时调用handleEvent方法。

无论你使用哪种方式给DOM元素绑定事件,它都会得到一个事件对象作为第一参数。该对象包含发生了什么事情的详细信息。

另外外们可以在同一个元素上绑定多个事件,其实我们也可以在多个元素上绑定同一个事件,下一节我们将来一起探讨如何在多个元素上绑定同一个事件。Nike Unveil a Dip Dye-Inspired Air VaporMax Plus