DOM模型(六)—— 事件模型

事件是一种异步编程的实现方式,本质上是程序各个组成部分之间的通信。
DOM支持大量的事件,本节介绍DOM的事件编程。

一、EventTarget接口(即三个方法)


DOM的事件操作(监听和触发),都定义在EventTarget接口。Element节点document节点window对象,都部署了这个接口。此外,XMLHttpRequest、AudioNode、AudioContext等浏览器内置对象,也部署了这个接口。

该接口就是三个方法。

  • addEventListener:绑定事件的监听函数。
  • removeEventListener:移除事件的监听函数。
  • dispatchEvent:触发事件。

1.1、addEventListener()

addEventListener方法用于在当前节点或对象上,定义一个特定事件的监听函数。

//使用格式
target.addEventListener(type, listener[, useCapture]);

//实例
window.addEventListener('load', function(){...}, false);

addEventListener方法接受三个参数。

  • type:事件名称,大小写敏感。
  • listener:监听函数。事件发生时,会调用该监听函数。
  • useCapture:布尔值,表示监听函数是否在捕获阶段(capture)触发,默认为false(监听函数只在冒泡阶段被触发)。老式浏览器规定该参数必写,较新版本的浏览器允许该参数可选。为了保持兼容,建议总是写上该参数。

下面是一个例子。

function hello() {
  console.log('Hello world');
}

var button = document.getElementById('btn');
button.addEventListener('click', hello, false);

上面代码中,addEventListener方法为button元素节点,绑定click事件的监听函数hello,该函数只在冒泡阶段触发。

addEventListener方法可以为当前对象的同一个事件,添加多个监听函数。这些函数按照添加顺序触发,即先添加先触发。如果为同一个事件多次添加同一个监听函数,该函数只会执行一次,多余的添加将自动被去除(不必使用removeEventListener方法手动去除)。

function hello(){
    console.log('Hello world');
}

document.addEventListener('click', hello, false);  //同一事件多次添加同一监听函数
document.addEventListener('click', hello, false);

执行上面代码,点击文档只会输出一行Hello world。

如果希望向监听函数传递参数,可以用匿名函数包装一下监听函数。

function print(x){
    console.log(x);
}

var el = document.getElementById('div1');
el.addEventListener('click', function(){
    print('Hello');
}, false);

上面代码通过匿名函数,向监听函数print传递了一个参数。

1.2、removeEventListener()

removeEventListener方法用来移除addEventListener方法添加的事件监听函数。

div.addEventListener('click', listener, false);
div.removeEventListener('click', listener, false);

removeEventListener方法的参数,与addEventListener方法完全一致。它的第一个参数“事件类型”,大小写敏感。

注意,removeEventListener方法移除的监听函数,必须与对应的addEventListener方法的参数完全一致,而且必须在同一个元素节点,否则无效。

1.3、dispatchEvent()

dispatchEvent方法在当前节点上触发指定事件,从而触发监听函数的执行。该方法返回一个布尔值,只要有一个监听函数调用了Event.preventDefault(),则返回值为false,否则为true。

target.dispatchEvent(event)

dispatchEvent方法的参数是一个Event对象的实例。

para.addEventListener('click', hello, false);
var event = new Event('click');
para.dispatchEvent(event);

上面代码在当前节点触发了click事件。

如果dispatchEvent方法的参数为空,或者不是一个有效的事件对象,将报错。

下面代码根据dispatchEvent方法的返回值,判断事件是否被取消了。

var canceled = !cb.dispatchEvent(event);
  if (canceled) {
    console.log('事件取消');
  } else {
    console.log('事件未取消');
  }
}

二、监听函数


监听函数(listener)是事件发生时,程序所要执行的函数。它是事件驱动编程模式的主要编程方式。

DOM提供三种方法,可以用来为事件绑定监听函数。

2.1、HTML标签的on-属性

HTML语言允许在元素标签的属性中,直接定义某些事件的监听代码。

<body onload="doSomething()">
<div onclick="console.log('触发事件')">

上面代码为body节点的load事件、div节点的click事件,指定了监听函数。

使用这个方法指定的监听函数,只会在冒泡阶段触发。

注意,使用这种方法时,on-属性的值是将会执行的代码,而不是一个函数。

<!-- 正确 -->
<body onload="doSomething()">

<!-- 错误 -->
<body onload="doSomething">

一旦指定的事件发生,on-属性的值是原样传入JS引擎执行。因此如果要执行函数,不要忘记加上一对圆括号。

另外,Element元素节点的setAttribute方法,其实设置的也是这种效果。

Element.setAttribute('onclick', 'doSomething()');

2.2、Element节点的事件属性

Element节点对象有事件属性,同样可以指定监听函数。

window.onload = doSomething;

div.onclick = function(event){
    console.log('触发事件');
};

使用这个方法指定的监听函数,只会在冒泡阶段触发。

2.3、addEventListener方法

通过Element节点、document节点、window对象的addEventListener方法,也可以定义事件的监听函数。

window.addEventListener('load', doSomething, false);

在上面三种方法中,第一种“HTML标签的on-属性”,违反了HTML与JS代码相分离的原则;第二种“Element节点的事件属性”的缺点是,同一个事件只能定义一个监听函数,也就是说,如果定义两次onclick属性,后一次定义会覆盖前一次。因此,这两种方法都不推荐使用,除非是为了程序的兼容问题,因为所有浏览器都支持这两种方法。

addEventListener是推荐的指定监听函数的方法。它的优点如下:

  • 可以针对同一个事件,添加多个监听函数
  • 能够指定在哪个阶段(捕获阶段还是冒泡阶段)触发监听函数
  • 除了DOM节点,还可以部署windowXMLHttpRequest等对象上面,等于统一了整个JS的监听函数接口。

2.4、this对象的指向

实际编程中,监听函数内部的this对象,常常需要指向触发事件的那个Element节点。

addEventListener方法指定的监听函数,内部的this对象总是指向触发事件的那个节点。

//HTML代码
//<p id="para">Hello</p>

var id = 'doc';
var para = document.getElementById('para');

function hello(){
    console.log(this.id);
}

para.addEventListener('click', hello, false);

执行上面代码,点击<p>节点会输出para。这是因为监听函数被拷贝成了节点的一个属性,所以this指向节点对象。使用下面的写法,会看得更清楚。

para.onclick = hello;

如果将监听函数部署在Element节点的on-属性上面,this不会指向触发事件的元素节点。

<p id="para" onclick="hello()">Hello</p>
<!-- 或者使用JavaScript代码  -->
<script>
  pElement.setAttribute('onclick', 'hello()');
</script>

执行上面代码,点击<p>节点会输出doc。这是因为这里只是调用hello函数,而hello函数实际是在全局作用域执行,相当于下面的代码。

para.onclick = function () {
  hello();
}

一种解决方法是,不引入函数作用域,直接在on-属性写入所要执行的代码。因为on-属性是在当前节点上执行的。

<p id="para" onclick="console.log(id)">Hello</p>
<!-- 或者 -->
<p id="para" onclick="console.log(this.id)">Hello</p>

上面两行,最后输出的都是para

总结一下,以下写法的this对象都指向Element节点。

// JavaScript代码
element.onclick = print
element.addEventListener('click', print, false)
element.onclick = function () {console.log(this.id);}

// HTML代码
<element onclick="console.log(this.id)">

以下写法的this对象,都指向全局对象。

// JavaScript代码
element.onclick = function (){ doSomething() };
element.setAttribute('onclick', 'doSomething()');

// HTML代码
<element onclick="doSomething()">

三、事件的传播


3.1、传播的三个阶段

当一个事件发生后,它会在不同的DOM节点之间传播(propagation)。这种传播分成三个阶段:

  • 第一阶段:从window对象传导到目标节点,称为“捕获阶段”(capture phase)。
  • 第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。
  • 第三阶段:从目标节点传导回window对象,称为“冒泡阶段”(bubbling phase)。

这种三阶段的传播模型,会使得一个事件在多个节点上触发(只要节点上绑定对应事件的监听函数)。比如,假设点击<div>之中嵌套一个<p>节点。

<div>
    <p>Click Me</p>
</div>

如果对这两个节点的click事件都设定监听函数,则click事件会被触发四次。

var phase = {
     1: 'capture',
     2: 'target',
     3: 'bubble'
};

var div = document.querySelector('div');
var p = document.querySelector('p');

div.addEventListener('click', callback, true);  //函数只在捕获阶段被触发
p.addEventListener('click', callback, true);
div.addEventListener('click', callback, false);  //函数只在冒泡阶段被触发
p.addEventListener('click', callback, false);

function callback(event){
    var tag = event.currentTarget.tagName;
    var phase = phases[event.eventPhase];
    console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}

// 点击以后的结果
// Tag: 'DIV'. EventPhase: 'capture'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'DIV'. EventPhase: 'bubble'

上面的代码表示,click事件被触发了四次:<p>节点的捕获阶段和冒泡阶段各一次,<div>节点的捕获和冒泡阶段各一次。

1、捕获阶段:事件从<div><p>传播时,触发<div>click事件;
2、目标阶段:事件从<div>到达<p>时,触发<p>click事件;
3、目标阶段:事件离开<p>时,触发<p>click事件;
4、冒泡阶段:事件从<p>传回<div>时,再次触发<div>click事件。

注意,用户点击网页的时候,浏览器总是假定click事件的目标节点,就是点击位置的嵌套最深的那个节点(嵌套在<div>节点的<p>节点)。所以,<p>节点的捕获阶段和冒泡阶段,都会显示为target阶段。

事件传播的最上层对象是window,接着依次是documenthtmldocument.documentElement)和bodydocument.dody)。也就是说,如果<body>元素中有一个<div>元素,点击该元素。事件的传播顺序,在捕获阶段依次为windowdocumenthtmlbodydiv,在冒泡阶段依次为divbodyhtmldocumentwindow

3.2、事件的代理

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。

var ul = document.querySelector('ul');

ul.addEventListener('click', function(event) {
  if (event.target.tagName.toLowerCase() === 'li') {
    // some code
  }
});

上面代码的click事件的监听函数定义在<ul>节点上,但是实际上,它处理的是子节点<li>click事件。这样做的好处是,只要定义一个监听函数,就能处理多个子节点的事件,而且以后再添加子节点,监听函数依然有效。

如果希望事件到某个节点为止,不再传播,可以使用事件对象的stopPropagation方法。

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

使用上面的代码以后,click事件在冒泡阶段到达<p>节点以后,就不再向上(父节点的方向)传播了。

但是,stopPropagation方法只会阻止当前监听函数的传播,不会阻止<p>节点上的其他click事件的监听函数。如果想要不再触发那些监听函数,可以使用stopImmediatePropagation方法。

p.addEventListener('click', function(event) {
 event.stopImmediatePropagation();
});

p.addEventListener('click', function(event) {
 // 不会被触发
});

四、Event对象


事件发生以后,会生成一个事件对象,作为参数传给监听函数。浏览器原生提供一个Event对象,所有的事件都是这个对象的实例,或者说继承了Event.prototype对象。

Event对象本身就是一个构造函数,可以用来生成新的实例。

event = new Event(typeArg, eventInit);

4.1、event.bubbles,event.eventPhase

以下属性与事件的阶段有关。
(1)、bubbles
bubbles属性返回一个布尔值,表示当前事件是否会冒泡。该属性为只读属性,只能在新建事件时改变。除非显示声明,Event构造函数生成的事件,默认是不冒泡的。

function goInput(e) {
  if (!e.bubbles) {
    passItOn(e);
  } else {
    doOutput(e);
  }
}

上面代码根据事件是否冒泡,调用不同的函数。

(2)、eventPhase
eventPhase属性返回一个整数值,表示事件目前所处的节点。

var phase = event.eventPhase;

0,事件目前没有发生。
1,事件目前处于捕获阶段,即处于从祖先节点向目标节点的传播过程中。该过程是从window对象到document节点,再到HTMLHtmlElement节点,直到目标节点的父节点为止。
2,事件到达目标节点,即target属性指向的那个节点。
3,事件处于冒泡阶段,即处于从目标节点向祖先节点的反向传播过程中。该过程是从父节点一直到window对象。只有bubbles属性为true时,这个阶段才可能发生。

4.2、event.cancelable,event.defaultPrevented

以下属性与事件的默认行为有关。
(1)、cancelable
cancelable属性返回一个布尔值,表示事件是否可以取消。该属性为只读属性,只能在新建事件时改变。除非显示声明,Event构造函数生成的事件,默认是不可以取消的。

var bool = event.cancelable;

如果要取消某个事件,需要在这个事件上面调用preventDefault方法,这会阻止浏览器对某种事件部署的默认行为。

(2)、defaultPrevented
defaultPrevented属性返回一个布尔值,表示该事件是否调用过defaultPrevented方法。

if (e.defaultPrevented) {
  // ...
}

4.3、event.currentTarget,event.target

以下属性与事件的目标节点有关。
(1)、currentTarget

currentTarget属性返回事件当前所在的节点,即正在执行的监听函数所绑定的那个节点。作为比较,target属性返回事件发生的节点。

如果监听函数在捕获阶段和冒泡阶段触发,那么这两个属性返回值是不一样的。

function hide(e){
  console.log(this === e.currentTarget);  // true
  e.currentTarget.style.visibility = "hidden";
}

para.addEventListener('click', hide, false);

上面代码中,点击para节点,该节点会不可见。另外,在监听函数中,currentTarget属性实际上等同于this对象。

(2)、target
target属性返回触发事件的那个节点,即事件最初发生的节点。如果监听函数不在该节点触发,那么它与currentTarget属性返回的值是不一样的。

function hide(e){
  console.log(this === e.target);  // 有可能不是true
  e.target.style.visibility = "hidden";
}

// HTML代码为
// <p id="para">Hello <em>World</em></p>
para.addEventListener('click', hide, false);

4.4、event.type,event.detail,event.timeStamp,event.isTrusted

以下属性与事件对象的其他信息相关。
(1)、type
type属性返回一个字符串,表示事件类型,大小写敏感。

var string = event.type;

(2)、detail
detail属性返回一个数值,表示事件的某种信息。具体含义与事件类型有关,对于鼠标事件,表示鼠标按键在某个位置按下的次数,比如对于dbclick事件,detail属性的值总是2。

(3)、timeStamp
timeStamp属性返回一个毫秒时间戳,表示事件发生的时间。

var number = event.timeStamp;

(4)、isTrusted
isTrusted属性返回一个布尔值,表示该事件是否为真实用户触发。

var bool = event.isTrusted;

用户触发的事件返回true,脚本触发的事件返回false

4.5、event.preventDefault()

preventDefault方法取消浏览器对当前事件的默认行为,比如点击链接后,浏览器跳转到指定页面,或者按一下空格键,页面向下滚动一段距离。该方法生效的前提是,事件的cancelable属性为true,如果为false,则调用该方法没有任何效果。

该方法不会阻止事件的进一步传播(stopPropagation方法可用于这个目的)。只要在事件的传播过程中(捕获阶段、目标阶段、冒泡阶段皆可),使用了preventDefault方法,该事件的默认方法就不会执行。

// HTML代码为
// <input type="checkbox" id="my-checkbox" />

var cb = document.getElementById('my-checkbox');

cb.addEventListener(
  'click',
  function (e){ e.preventDefault(); },
  false
);

上面代码为点击单选框的事件,设置监听函数,取消默认行为。由于浏览器的默认行为是选中单选框,所以这段代码会导致无法选中单选框。

如果监听函数最后返回布尔值false(即return false),浏览器也不会触发默认行为,与preventDefault方法有等同效果。

4.6、event.stopPropagation()

stopPropagation方法阻止事件在DOM中继续传播,防止再触发定义在别的节点上的监听函数,但是不包括在当前节点上新定义的事件监听函数。

function stopEvent(e) {
  e.stopPropagation();
}

el.addEventListener('click', stopEvent, false);

将上面函数指定为监听函数,会阻止事件进一步冒泡到el节点的父节点。

4.7、event.stopImmediatePropagation()

stopImmediatePropagation方法阻止同一个事件的其他监听函数被调用。

如果同一个节点对于同一个事件指定了多个监听函数,这些函数会根据添加的顺序依次调用。只要其中一个监听函数调用了stopImmediatePropagation方法,其他的监听函数就不会再执行了。

function l1(e){
  e.stopImmediatePropagation();
}

function l2(e){
  console.log('hello world');
}

el.addEventListener('click', l1, false);
el.addEventListener('click', l2, false);

上面代码的el节点上,为click事件添加了两个监听函数l1l2。由于l1调用了stopImmediatePropagation方法,所以l2不会被调用。

五、自定义事件和事件模拟


5.1、CustomEvent()

5.2、事件的模拟

5.3、自定义事件的老式写法

5.4、事件模拟的老式写法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容