事件模型是JS里十分重要的一个部分,这两天查询了一些相关资料,用自己的文字记录一下。本文不讨论IE。
事件模型
DOM事件模型
最简单且兼容所有浏览器的事件模型,有两种方式。默认发生在冒泡阶段。
- HTML中直接绑定(不推荐)
<button id="button" onclick="click()">ClickMe</button>
- JS指定属性值
var button = document.getElementById("button") button.onclick = function() { //... }
DOM 2级模型
属于W3C标准模型,先事件捕获,到达目标后再进行冒泡。兼容现代浏览器。
//DOM 2级事件第三个参数是一个布尔值(默认为false),true表示捕获阶段调用事件处理程序,false表示冒泡阶段调用事件处理程序。 var button = document.getElementById("button") button.addEventListener('click', function() { //... }, false)
DOM 0级和DOM 2级事件模型比较
- 移除监听函数
button.onclick = null button.removeEventListener('click', click, false) //注意,addEventListener()添加的匿名函数无法移除
- 同时绑定多个监听器
button.onclick = function() { console.log(1) } button.onclick = function() { console.log(2) } button.addEventListener('click', function() { console.log(3) }, false) button.addEventListener('click', function() { console.log(4) }, false) // 2 3 4
可见DOM0 级后绑定的函数会把前边的替换掉,而DOM 2级可以同时绑定多个监听器,因此推荐使用addEventListener方法监听事件。
- 监听函数内部的this都指向触发事件的那个元素节点。
<button id="button" onclick="console.log(this.id)"></button> var button = document.getElementById("button") button.onclick = function() { console.log(this.id) } button.addEventListener('click', function() { console.log(this.id) }, false) //输出结果均为"button"
事件流
首先我们要先了解一下事件流。事件流描述的是在页面中接受的事件顺序。看下面的例子
<!-- HTML --> <div id="container"> <button id="button"></button> </div> //JS var container = document.getElementById("container") var button = document.getElementById("button") button.onclick = function() {alert('button!')} container.onclick = function() {alert('container!')}
当你点击button按钮时,会发生在button按钮上的单击事件,但同时,你也单击了container元素甚至是单击了整个页面。JS事件模型是一种观察者模式(又叫发布订阅者模式),当对应的事件被触发时,监听该事件的所有监听函数都会被调用。因此,如果你单击的多个元素都绑定了相同事件,他们执行的事件顺序是怎么样的呢?
事件流有两种:
事件冒泡:事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接受,然后逐渐向上传播到较不具体的那个节点。
<button> -> <#container> -> <body> -> <html> -> document
事件捕获:与事件冒泡相反。事件最开始由不太具体的节点最早接受事件, 而最具体的节点最后接受事件。
document -> <html> -> <body> -> <#container> -> <button>
在这个例子中,事件是通过DOM 0级事件(默认发生在冒泡阶段)添加的,所以弹出button!后弹出container!。
要让事件发生在捕获阶段也很简单
//DOM 2级事件第三个参数是一个布尔值(默认为false),true表示捕获阶段调用事件处理程序,false表示冒泡阶段调用事件处理程序。 button.addEventListener('click', function(){ alert('button!') }, true) container.addEventListener('click', function(){ alert('container!') }, true)
事件对象
触发DOM上的事件后,会产生一个事件对象event,作为参数传给监听函数。所有的事件都是这个事件对象的示例。
- 事件对象常用属性type 被触发的事件的类型target 事件的目标currentTarget 注册这个事件监听的对象事件对象常用方法preventDefault() 取消事件的默认行为stopPropagation() 阻止事件继续传播(冒泡和捕获),不包括在当前节点上其他的事件监听函数。stopImmediatePropagation() 阻止所有事件继续传播,包括在当前节点上其他的事件监听函数。
了解更多事件对象的属性和方法 事件对象
看到这里,尝试一下实现页面内点击弹窗外部关闭弹窗,点击弹窗内部不会关闭弹窗。
<body> <div id="pop-up-window"></div> </body> <script> var body = document.querySelector('body') var popUpWindow = document.getElementById('pop-up-window') body.addEventListener('click', function(e) { popUpWindow.style.display = 'none' }, false) popUpWindow.addEventListener('click', function(e) { e.stopPropagation() //在弹窗内部点击时阻止事件传播,因此不会触发body的click事件 }, false) </script>
事件委托
借助事件冒泡和事件对象,可以实现事件委托(又叫事件代理)。
先思考如何给下面的按钮都绑定一个click事件,点击后输出按钮的id
<div id="contiainer"> <button id="button1">button1</button> <button id="button2">button2</button> <button id="button3">button3</button> <!-- ... --> <button id="button10">button10</button> </div>
或许你或你以前会这么写
for (var i = 1; i <= 10; i++) { document.getElementById("button" + i).addEventListener('click', function(e) { console.log(e.target.id) }, false) }
每个函数都是对象,都会占用内存,现在创建了10个监听事件,影响了页面性能。我们利用事件委托(又叫事件代理)可以解决这个问题。事件委托借助事件冒泡和事件对象,只需要创建一个监听器,就可以管理一个类型的所有事件。只需要在DOM树尽量最高的层次创建一个监听。
document.getElementById('container').addEventListener('click', function(e) { if (e.target.tagName.toLowerCase() === 'button') console.log(e.target.id) }, false)