JavaScript 浏览器事件机制(捕获、冒泡、委托)

802 阅读6分钟

DOM 树

文档对象模型 (DOM),我们把它简单的理解成一个对象const DOMTree = {}。它是浏览器的解析引擎把HTML文档解析成相应的JavaScript对象。

<!DOCTYPE HTML>
<html>
<head>
  <title>About elk</title>
</head>
<body>
  The truth about elk.
</body>
</html>

最终会解析成的树状结构对象:

DOM 对象提供了一些接口让我们可以对节点进行增删改查。并且还可以让我们对节点进行绑定事件。 例如我们可以对<body></body>节点绑定一个点击事件。当用户触发了点击事件后,可以让节点响应一些事情。

DOM 事件

DOM 事件提供了人与web进行交互的一种方式。

事件流

如果你单击了某个按钮,他们都认为单击事件不仅仅发生在按钮上。换句话说,在单击按钮的同时,你也单击了按钮的容器元素,甚至也单击了整个页面。

事件流描述的是从页面中接收事件的顺序。但有意思的是,IE和Netscape开发团队居然提出了差不多是完全相反的事件流的概念。IE的事件流是事件冒泡流,而Netscape Communicator的事件流是事件捕获流。

事件冒泡

IE的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。

<html>
<head>
  <title></title>
</head>
<body>
  <div onclick="alert('Clicked')">点击我</div>   
</body>
</html>

click 事件首先在<div>元素上发生,而这个元素就是我们单击的元素。然后,click事件沿DOM树向上传播,在每一级节点上都会发生,直至传播到document对象。图片展示了事件冒泡的过程

[注意]所有现代浏览器都支持事件冒泡。

事件捕获

事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。

在事件捕获过程中,document对象首先接收到click事件,然后事件沿DOM树依次向下,一直传播到事件的实际目标,即<div>元素。图片展示了事件捕获的过程。

DOM 事件流

“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。

在DOM事件流中,实际的目标(<div>元素)在捕获阶段不会接收到事件。这意味着在捕获阶段,事件从document到<html>再到<body>后就停止了。

下一个阶段是“处于目标”阶段,于是事件在<div>上发生,并在事件处理中被看成冒泡阶段的一部分。

然后,冒泡阶段发生,事件又传播回文档。

多数支持DOM事件流的浏览器都实现了一种特定的行为;即使“DOM2级事件”规范明确要求捕获阶段不会涉及事件目标,但IE9、Safari、Chrome、Firefox和Opera 9.5及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两个机会在目标对象上面操作事件。

[兼容]IE9、Opera、Firefox、Chrome和Safari都支持DOM事件流;IE8及更早版本不支持DOM事件流。

事件处理程序

事件就是用户或浏览器自身执行的某种动作。诸如 click、load 和 mouseover,都是事件的名字。 而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。

HTML 事件处理程序

<input type="button" value="Click Me" onclick="alert('Clicked')" >
<input type="button" value="Click Me" onclick="alert(this.value)">

html中定义的事件有权访问全局作用域中的任何代码,例如外部引入的JS文件

DOM0级事件处理程序

以DOM0级方式添加的事件处理程序会在事件流的冒泡阶段被处理

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    alert("Clicked");
};

使用DOM0级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;

DOM0级事件处理程序的缺点是围绕着每个事件目标对于每种事件类型只能添加一个事件处理程序。

DOM2级事件处理程序

DOM2级事件处理程序定义了两个方法用于处理指定和删除事件处理程序的操作:addEventListener()removeEventListener()

所有DOM节点中都包含这两个方法,并且它们都接受3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。

最后的布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。若最后的布尔值不填写,则和false效果一样

使用DOM2级事件处理程序的好处是可以添加多个事件处理程序,并按照他们添加的顺序触发

<div id="box" style="height:30px;width:200px;background-color:pink;"></div>
<script>
    // 添加事件绑定
    box.addEventListener("click",function(){
        test('123');
    },false);

    function test(x){box.innerHTML += x;}
    
    // 移除事件绑定
    box.removeEventListener('click',test,false);    
</script>

[兼容]IE9+、Firefox、Safari、Chrome和Opera支持DOM2级事件处理程序

DOM3级事件

在DOM2级事件的基础上添加了更多的事件类型。

  • UI事件,当用户与页面上的元素交互时触发,如:load、scroll
  • 焦点事件,当元素获得或失去焦点时触发,如:blur、focus
  • 鼠标事件,当用户通过鼠标在页面执行操作时触发如:dblclick、mouseup
  • 滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
  • 文本事件,当在文档中输入文本时触发,如:textInput
  • 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
  • 合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart
  • 变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified

同时DOM3级事件也允许使用者自定义一些事件。

IE中的DOM2级事件处理程序(<=IE8)

IE实现了与DOM中类似的两个方法:attachEvent()和detachEvent()。 这两个方法接受相同的两个参数:事件处理程序名称与事件处理程序函数。

var btn = document.getElementById("myBtn");
var handler = function(){
    alert("Clicked");
};

// 绑定事件
btn.attachEvent("onclick", handler);

// 移除事件
btn.detachEvent("onclick", handler);
  • 由于IE8及更早版本只支持事件冒泡,所以通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段。
  • attachEvent()的第一个参数是"onclick",而非addEventListener()方法中的"click"。
  • 事件处理程序会在全局作用域中运行,因此this等于window。

兼容IE低版本事件处理程序

var EventUtil = {
    addHandler: function(element, type, handler){
        if (element.addEventListener){
            element.addEventListener(type, handler, false);
        } else if (element.attachEvent){
            element.attachEvent("on" + type, handler);
        } else {
            element["on" + type] = handler;
        }
    },
    removeHandler: function(element, type, handler){
        if (element.removeEventListener){
            element.removeEventListener(type, handler, false);
        } else if (element.detachEvent){
            element.detachEvent("on" + type, handler);
        } else {
            element["on" + type] = null;
        }
    }
};

事件的 Event 对象

在触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。

包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件对象中,会包含鼠标位置的信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。所有浏览器都支持event对象,但支持方式不同。

兼容DOM的浏览器会将一个event对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法(DOM0级或DOM2级),都会传入event对象。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    alert(event.type);     //"click"
};
btn.addEventListener("click", function(event){
    alert(event.type);     //"click"
}, false);

通过这个事件对象我们可以做一些事情:

  1. 阻止浏览器的默认行为
  2. 阻止事件冒泡和捕获

阻止浏览器的默认行为

当你点击a标签时,浏览器有一个默认行为是跳转链接。 当我们想要自己去绑定事件,并且不让这个默认行为发生就叫阻止浏览器的默认行为

var a = document.getElementById("a");

a.addEventListener("click", function(event){
    event.preventDefault();
    // 事件处理程序内容
}, false);

阻止事件冒泡和捕获

前面已经讲了事件流的行为,那么当我们只想在目标阶段处理这个click事件,并且不希望该事件冒泡或捕获时可以阻止事件冒泡和捕获

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    alert("Clicked");
    event.stopPropagation(); // 阻止事件传播
};
// document中虽然监听了点击事件,由于点击btn时阻止了事件传播,因此并不会冒泡到document上
document.body.onclick = function(event){
    alert("Body clicked");
};

事件的内存和性能

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。

事件委托

对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click事件会一直冒泡到document层次。也就是说,我们可以为整个页面指定一个onclick事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。

<ul id="myLinks">
    <li id="goSomewhere">Go somewhere</li>
    <li id="doSomething">Do something</li>
    <li id="sayHi">Say hi</li>
</ul>

为3个li都添加相应的点击事件,这样会导致性能更差,我们可以只在ul上面添加一个点击事件,利用冒泡原理来处理事件

var list = document.getElementById("myLinks");

EventUtil.addHandler(list, "click", function(event){

    switch(event.target.id){
        case "doSomething":
            document.title = "I changed the document's title";
            break;

        case "goSomewhere":
            location.href = "http://www.wrox.com";
            break;

        case "sayHi":
            alert("hi");
            break;
    }
});

事实上React中的事件处理程序都是采用的事件委托的方式。