JavaScript——DOM与事件

DOM与事件

1、DOM选择器

DOM选择器用于快速定位DOM元素。在原生的JavaScript中有提供根据id、name等属性来查找的传统选择器,也有新型的、更高效的querySelector选择器和querySelectorAll选择器,支持丰富的元素、属性、内容选择等。

1.1、传统原生JavaScript选择器

HTML页面中主要是定义一系列的ul-li标签,然后通过DOM选择器定位li后并操作它们,初始代码如下所示。

html 复制代码
<ul>
    <li>节点1.1</li>
    <li>节点1.2</li>
    <li>节点1.3</li>
    <li>节点1.4</li>
    <li>节点1.5</li>
    <li>节点1.6</li>
    <li>节点1.7</li>
</ul>
<ul>
    <li>节点2.1</li>
    <li>节点2.2</li>
    <li>节点2.3</li>
    <li>节点2.4</li>
    <li>节点2.5</li>
    <li>节点2.6</li>
    <li>节点2.7</li>
</ul>
1.1.1、通过id定位

JavaScript提供了getElementById()函数,通过id定位元素,返回匹配到id的第一个元素。

当具有相同id的元素时,除了第一个元素能被匹配到外,其他元素都会被忽略。

js 复制代码
document.getElementById('one').innerText; // 节点1.1
1.1.2、通过class定位

JavaScript提供了getElementsByClassName()函数,通过类名定位元素,返回由匹配到的元素构成的HTMLCollection对象,它是一个类数组结构。

html 复制代码
<li class="one">节点1.2</li>
<li class="one">节点1.3</li>
js 复制代码
document.getElementsByClassName('one');

返回值为一个HTMLCollection对象,里面包含匹配到的两个li元素值。

js 复制代码
HTMLCollection(2) [li.one, li
 - 0: li.one
 - 1: li.one
 - length: 2
 -  _ _proto_ _:  HTMLCollection
1.1.3、通过name定位

JavaScript提供了getElementsByName()函数,通过元素的name属性进行定位,返回由匹配到的元素构成的NodeList对象,它是一个类数组结构。

html 复制代码
<ul>
    <li id="one">节点1.1</li>
    <li name="node">节点1.4</li>
    <li name="node">节点1.5</li>
</ul>
<ul>
    <li name="node">节点2.1</li>
    <li>节点2.2</li>
</ul>
js 复制代码
document.getElementsByName('node');

返回的值为一个NodeList对象,里面包含匹配到的name属性为"node"的元素。

js 复制代码
NodeList(3) [li, li, li]
 - 0: li
 - 1: li
 - 2: li
 - length: 3
 -  _ _proto_ _:  NodeList
1.1.4、通过标签名定位

JavaScript提供了getElementsByTagName()函数,通过标签名定位元素,返回由匹配到的元素构成的HTMLCollection对象。我们通过标签名获取页面上的两个ul元素。

js 复制代码
document.getElementsByTagName('ul');

返回值为一个HTMLCollection对象,里面包含匹配到的两个ul元素值。

js 复制代码
HTMLCollection(2) [ul, ul]
- 0: ul
- 1: ul
- length: 2
- _ _proto_ _: HTMLCollection

document.all在早期是IE支持的属性,而现在的浏览器也都提供了支持,但是在实现细节上有些差异。因此,获取元素还是推荐用W3C DOM规范中提供的 document.getElementById()、document.getElementsByName()等标准函数。

1.2、新型的querySelector选择器和querySelectorAll选择器

我们使用传统的id、name、class等选择器来查找元素时,只能调用document具有的函数,在查找特定元素的子元素时不太方便。

为了能更高效地使用选择器,让其定位到特定的元素或者子元素中,于是诞生了新型的querySelector选择器和querySelectorAll选择器。

querySelector选择器和querySelectorAll选择器是在W3C DOM4中新增的,都是按照CSS选择器的规范来实现的,接下来分别来看它们的具体使用方法。

1.2.1、querySelector选择器

querySelector选择器返回的是在基准元素下,选择器匹配到的元素集合中的第一个元素。语法如下所示。

js 复制代码
element = baseElement.querySelector(selectors);

其中,baseElement是基准元素,返回的元素必须是匹配到的基准元素的第一个子元素。该基准元素可以为Document,也可以为基本的Element。

selectors是一个标准的CSS选择器,而且必须是合法的选择器,否则会引起语法错误。

返回值为匹配到的第一个子元素。匹配的过程中不仅仅针对基准元素的后代元素,实际上会遍历整个文档结构,包括基准元素和它的后代元素以外的元素。实际处理过程是首先创建一个匹配元素的初始列表,然后判断每个元素是否为基准元素的后代元素,第一个属于基准元素的后代元素将会被返回。

html 复制代码
<div>
  <h5>Original content</h5>
  <span>outside span</span>
  <p class="content">
    inside paragraph
    <span>inside span</span>
    inside paragraph
</p>
  </div>

获取p元素的第一个span元素:

js 复制代码
document.querySelector('p span').innerText; // inside span

获取class为content的元素的第一个span元素:

js 复制代码
document.querySelector('.content span').innerText; // inside span

获取第一个span或者h5元素:

js 复制代码
document.querySelector('h5, span').innerText // Original content

然后通过以下这段代码来验证上述返回值的匹配过程。

js 复制代码
var baseElement = document.querySelector("p");
console.log(baseElement.querySelector("div span").innerText);

代码最终输出的结果为"inside span"​。

第一行代码获取的基准元素为p元素,第二行代码中的选择器为"divspan"​。虽然在p元素中没有div元素,却依旧能匹配到span元素。这是因为在匹配过程会优先找出div元素下span元素的集合,然后判断span元素是否属于p元素的子元素,最后返回第一个匹配到的span元素值。

1.2.2、querySelectorAll选择器

querySelectorAll选择器与querySelector选择器类似,区别在于 querySelectorAll选择器会返回基准元素下匹配到的所有子元素的集合。

js 复制代码
elementList = baseElement.querySelectorAll(selectors);

它同样包含基准元素与选择器,返回值是一个NodeList的集合。接下来通过一系列代码来看querySelectorAll选择器的使用方法。

html 复制代码
<div>
    <h5>Original content</h5>
    <span>outside span</span>
    <p class="content">
        inside paragraph
        <span>inside span</span>
        inside paragraph
    </p>
</div>

获取所有的span元素:

js 复制代码
document.querySelectorAll('span');

其返回值如下所示

js 复制代码
NodeList(2) [span, span]
 - 0: span
 - 1: span
 - length: 2
 -  _ _proto_ _:  NodeList

querySelectorAll选择器匹配过程与querySelector选择器一样,优先获取所有匹配元素的集合,然后判断每个元素是否属于基准元素。如果属于则返回结果,最终返回一个NodeList对象。

接下来通过以下代码来理解querySelectorAll选择器匹配元素的过程。

html 复制代码
<div id="my-id">
    <img id="inside">
    <div class="lonely"></div>
    <div class="outer">
        <div class="inner"></div>
    </div>
</div>
js 复制代码
<script>
    var firstArr = document.querySelectorAll('#my-id div div');
        var secondArr = document.querySelector('#my-id').querySelectorAll('div div');
    console.log(firstArr);
    console.log(secondArr);
</script>

上面代码的主要目的是找出在id为"my-id"元素的子div中子div元素的集合。分别使用两种写法获取到了firstArr和secondArr两个值。

提问:firstArr和secondArr返回的NodeList值是否是一样的?

答案:不一样。实际上它们输出的结果如下所示。

js 复制代码
firstArr   NodeList [div.inner]
        - 0: div.inner
        - length: 1
        - _ _proto_ _: NodeList
secondArr  NodeList(3) [div.lonely, div.outer, div.inner]
        - 0: div.lonely
        - 1: div.outer
        - 2: div.inner
        - length: 3
        - _ _proto_ _: NodeList

我们可以看出firstArr表示的NodeList对象的长度为1,而secondArr表示的NodeList对象的长度为3,这是为什么呢?

针对firstArr,querySelectorAll选择器的调用方是document,则基准元素为document;执行CSS选择器,匹配到的元素只有一个,如下代码所示。

html 复制代码
<div class="inner"></div>

该元素属于document中的子元素,最终返回结果如下所示。

js 复制代码
NodeList [div.inner]

针对secondArr,先通过querySelector选择器确定基准元素是id为"my-id"的元素,然后执行CSS选择器,选择器的内容是匹配div元素中的子div元素。在189页下方的HTML代码对应的文档结构内,有3个元素是匹配的。

id为"my-id"的div元素的第一个子节点。

html 复制代码
<div class="lonely"></div>

id为"my-id"的div元素的第二个子节点。

html 复制代码
<div class="outer">...</div>

class为"outer"的div元素的第一个子节点。

html 复制代码
<div class="inner"></div>

紧接着判断这3个匹配的元素是否为基准元素的子元素,发现它们都是处于基准元素内部的,最终这3个值构成一个NodeList集合返回。

js 复制代码
NodeList(3) [div.lonely, div.outer, div.inner]

2、HTMLCollection对象与NodeList对象

不同的DOM选择器,它们的返回值有些是NodeList对象,有些是HTMLCollection对象。那么它们有什么区别呢?

html 复制代码
<div id="main">
    <p class="first">first</p>
    <p class="second">second<span>content</span></p>
</div>
<script>
    var main = document.getElementById("main");
    console.log(main.children);
    console.log(main.childNodes);
</script>

得到的结果如下所示。

js 复制代码
HTMLCollection(2) [p.first, p.second]
 - 0: p.first
 - 1: p.second
 - length: 2
 -  _ _proto_ _:  HTMLCollection
NodeList(5) [text, p.first, text, p.second, text]
 - 0: text
 - 1: p.first
 - 2: text
 - 3: p.second
 - 4: text
 - length: 5
 -  _ _proto_ _:  NodeList

从结果可以看出,调用children属性,返回的是HTMLCollection对象,其中包含两个元素p.first和p.second;调用childNodes属性,返回的是NodeList对象,其中包含了text、p.first、text、p.second、text 5个元素。

children属性和childNodes属性的不同在本质上是HTMLCollection对象和NodeList对象的不同。HTMLCollection对象与NodeList对象都是DOM节点的集合,但是在节点处理方式上是有差异的。接下来就深入了解两者的相同点和不同点。

2.1、HTMLCollection对象

HTMLCollection对象具有length属性,返回集合的长度,可以通过item ()函数和namedItem()函数来访问特定的元素。

item()函数:

HTMLCollection对象可以调用item()函数,通过序号来获取特定的某个节点,超过索引则返回"null"​。

html 复制代码
<div id="main">
    <p class="first">first</p>
    <p class="second">second</p>
    <p class="third">third</p>
    <p class="four">four</p>
</div>
<script>
    var main = document.getElementById("main").children;
    console.log(main.item(0));
    console.log(main.item(2));
</script>

通过item()函数定位第一个和第三个子元素,输出结果如下所示。

html 复制代码
<p class="first">first</p>
<p class="third">third</p>

namedItem()函数:

namedItem()函数用来返回一个节点。首先通过id属性去匹配,然后如果没有匹配到则使用name属性匹配,如果还没有匹配到则返回"null"​。当出现重复的id或者name属性时,只返回匹配到的第一个值。

html 复制代码
<form id="main">
    <input type="text" id="username">
    <input type="text" name="username">
    <input type="text" name="password">
</form>

<script>
    var main = document.getElementById("main").children;
    console.log(main.namedItem('username'));

</script>

在定义了id和name属性均为"username"值的两个元素后,最后输出的结果是id为"username"的元素。

html 复制代码
<input type="text" id="username">

2.2、NodeList对象

NodeList对象也具有length属性,返回集合的长度,也同样具有item()函数,通过索引定位子元素的位置。由于和HTMLCollection对象的item()函数一致,这里就不赘述了。

2.3、HTMLCollection对象和NodeList对象的实时性

HTMLCollection对象和NodeList对象并不是历史文档状态的静态快照,而是具有实时性的。对DOM树新增或者删除一个相关节点,都会立刻反映在HTMLCollection对象与NodeList对象中。

HTMLCollection对象与NodeList对象都只是类数组结构,并不能直接调用数组的函数。而通过call()函数和apply()函数处理为真正的数组后,它们就转变为一个真正的静态值了,不会再动态反映DOM的变化。

html 复制代码
<form id="main">
    <input type="text" id="username">
    <input type="text" name="password">
</form>

<script>
    // 获取HTMLCollection
    var mainChildren = document.getElementById('main').children;
        console.log(mainChildren.length);  // 2

    // 新增一个input元素
    var newInput = document.createElement('input');
    main.appendChild(newInput);
    console.log(mainChildren.length);  // 3

    // 通过call()函数处理成数组结构
    mainChildren = Array.prototype.slice.call(mainChildren, 0);
    mainChildren.splice(1, 1);
    console.log(mainChildren.length);  // 2

    // 再新增一个input元素
    var newInput2 = document.createElement('input');
    main.appendChild(newInput2);
    console.log(mainChildren.length);  // 2

</script>

最开始获取HTMLCollection对象的长度为2,新增一个input元素后,再输出的HTMLCollection对象的长度变为了3;然后将HTMLCollection对象通过call()函数处理成数组结构,删除第二个元素,HTMLCollection对象的长度变为2;最后再新增一个input元素,此时再获取HTMLCollection对象的长度,因为其已经变为一个静态的数组,并不能实时感知到DOM的变化,所以长度仍为2。

NodeList对象与HTMLCollection对象相比,存在一些细微的差异,主要表现在不是所有的函数获取的NodeList对象都是实时的。例如通过querySelectorAll()函数获取的NodeList对象就不是实时的。接下来通过下面这段代码来展示。

html 复制代码
<ul id="main">
    <li>文本1</li>
    <li>文本2</li>
    <li>文本3</li>
    <li>文本4</li>
    <li>文本5</li>
</ul>
js 复制代码
<script>
    // 获取ul
    var main = document.getElementById('main');
    // 获取li集合
    var lis = document.querySelectorAll('ul li');
    // 第一次输出li集合长度,值为5
    console.log(lis.length);

   // 新增li元素
    var newLi = document.createElement('li');
    var text = document.createTextNode('文本8');
    newLi.appendChild(text);
    main.appendChild(newLi);
    // 再次输出li集合长度,值为5
    console.log(lis.length);
    // 重新获取li的集合并输出长度,值为6
    console.log(document.querySelectorAll('ul li').length);
</script>

我们可以看出在新增一个li元素后,前后两次获取的li元素的集合的长度值均为5,并没有受到新增的newLi元素的影响。而在重新获取一次li元素的集合后,长度变为了6。

综上所述,HTMLCollection对象和NodeList对象具有以下的相同点和不同点。

相同点:

  • 都是类数组对象,有length属性,可以通过call()函数或apply()函数处理成真正的数组。
  • 都有item()函数,通过索引定位元素。
  • 都是实时性的,DOM树的变化会及时反映到HTMLCollection对象和NodeList对象上,只是在某些函数调用的返回结果上会存在差异。

不同点:

  • HTMLCollection对象比NodeList对象多个namedItem()函数,可以通过id或者name属性定位元素。
  • HTMLCollection对象只包含元素的集合(Element),即具有标签名的元素;而NodeList对象是节点的集合,既包括元素,也包括节点,例如text文本节点。

3、常用DOM操作

3.1、新增节点

html 复制代码
<ul id="container">
    <li class="first">文本1</li>
    <li class="second">文本2</li>
    <li>文本3</li>
    <li id="target">文本4</li>
    <li>文本5</li>
    <li>文本6</li>
</ul>
js 复制代码
//① 获取指定元素。
var container = document.querySelector('#container');
//② 新创建一个元素节点。
var newLiOne = document.createElement('li');
//③ 新创建一个属性节点,并设置值。
var newLiAttr = document.createAttribute('class');
newLiAttr.value = 'last';
//④ 将属性节点绑定在元素节点上。
newLiOne.setAttributeNode(newLiAttr);
//⑤ 新创建一个文本节点。
var newTextOne = document.createTextNode('新增文本1');
//⑥ 将文本节点作为元素节点的子元素。
newLiOne.appendChild(newTextOne);
//⑦ 使用appendChild()函数将新增元素节点添加至末尾。
container.appendChild(newLiOne);
//⑧ 新创建第二个元素节点。
var newLiTwo = document.createElement('li');
//⑨ 新创建第二个文本节点。
var newTextTwo = document.createTextNode('新增文本2');
//⑩ 将文本节点作为元素节点的子元素。
newLiTwo.appendChild(newTextTwo);
//⑪ 使用insertBefore()函数将节点添加至第一个新增节点的前面。
container.insertBefore(newLiTwo, newLiOne);

在新增属性节点时,还有另外一种更简单的setAttribute()函数。以上面代码为例,可以通过下面这一行代码完成上述③④这两步共3行代码的功能。

js 复制代码
newLiOne.setAttribute('class', 'last');

但是setAttribute()函数不兼容IE8及更早的版本,在使用时需要考虑到所使用的浏览器环境。

3.2、删除节点

删除节点的操作实际包含删除元素节点、删除属性节点和删除文本节点这3个操作。

html 复制代码
<ul id="main">
    <li>文本1</li>
    <li>文本2</li>
    <li>文本3</li>
</ul>
<a id="link" href="http://www.mianshiting.com">面试厅</a>

1. 删除ul的第一个li元素节点:

js 复制代码
//① 获取该元素的父元素。
var main = document.querySelector('#main');
//② 获取待删除节点。
var firstChild = main.firstElementChild;
//③ 通过父节点,调用removeChild()函数删除该节点。
main.removeChild(firstChild);

2.删除a标签的href属性:

js 复制代码
//① 获取该元素。
var link = document.querySelector('#link');
//② 通过元素节点,调用removeAttribute()函数删除指定属性节点。
link.removeAttribute('href');

3. 删除ul最后一个li元素的文本节点:

js 复制代码
//① 获取元素节点。
var lastChild = main.lastElementChild;
//② 获取文本节点。
var textNode = lastChild.childNodes[0];
//③ 通过元素节点,调用removeChild()函数删除指定的文本节点。
lastChild.removeChild(textNode);

关于删除文本节点还有一种比较简单的处理方法,那就是将元素节点的innerHTML属性设置为空。

js 复制代码
lastChild.innerHTML = '';

在删除文本节点时,我们更推荐使用设置innerHTML属性为空的方法。

3.3、修改节点

修改节点包含着很多不同类型的操作,包括修改元素节点、修改属性节点和修改文本节点。

html 复制代码
<div id="main">
    <!-- 测试修改元素节点 -->
    <div id="div1">替换之前的元素</div>
    <!-- 测试修改属性节点 -->
    <div id="div2" class="classA" style="color: green;">这是修改属性的节点</div>
    <!-- 测试修改文本节点 -->
    <div id="last">这是最后一个节点内容</div>
</div>

1. 修改元素节点

修改元素节点的操作一般是直接将节点元素替换为另一个元素,可以使用replaceChild()函数来实现。replaceChild()函数的调用方是父元素,接收两个参数,第一个参数表示新元素,第二个参数表示将要被替换的旧元素。

js 复制代码
<script>
    // 1.获取父元素与待替换的元素
    var main = document.querySelector('#main');
    var div1 = document.querySelector('#div1');
    // 2.创建新元素
    var newDiv = document.createElement('div');
    var newText = document.createTextNode('这是新创建的文本');
    newDiv.appendChild(newText);
    // 3.使用新元素替换旧的元素
    main.replaceChild(newDiv, div1);
</script>

2. 修改属性节点:

修改属性节点有两种处理方式:一种是通过getAttribute()函数和setAttribute()函数获取和设置属性节点值;另一种是直接修改属性名。第二种方式有个需要注意的地方是,直接修改的属性名与元素节点中的属性名不一定是一致的。就像class这个属性,因为它是JavaScript中的关键字,是不能直接使用的,所以需要使用className来代替。

js 复制代码
var div2 = document.querySelector('#div2');
// 方法1: 通过setAttribute()函数设置
div2.setAttribute('class', 'classB');
// 方法2: 直接修改属性名,注意不能直接用class,需要使用className
div2.className = 'classC';

// 方法1: 通过setAttribute()函数设置
div2.setAttribute('style', 'color: red;');
// 方法2: 直接修改属性名
div2.style.color = 'blue';

3. 修改文本节点:

修改文本节点与删除文本节点一样,将innerHTML属性修改为需要的文本内容即可。

js 复制代码
var last = document.querySelector('#last');
// 直接修改innerHTML属性
last.innerHTML = '这是修改后的文本内容';
//如果设置的innerHTML属性值中包含HTML元素,则会被解析
//使用如下代码进行验证
last.innerHTML = '<p style="color: red">这是修改后的文本内容</p>';
//在浏览器中渲染后,可以看到"这是修改后的文本内容"为红色

4、事件流

在浏览器中,JavaScript和HTML之间的交互是通过事件去实现的,常用的事件有代表鼠标单击的click事件、代表加载的load事件、代表鼠标指针悬浮的mouseover事件。在事件发生时,会相对应地触发绑定在元素上的事件处理程序,以处理对应的操作。

通常一个页面会绑定很多的事件,那么具体的事件触发顺序是什么样的呢?

这就会涉及事件流的概念,事件流描述的是从页面中接收事件的顺序。事件发生后会在目标节点和根节点之间按照特定的顺序传播,路径经过的节点都会接收到事件。我们通过下面的场景来直观地想象一下事件的流转顺序。

页面上有一个table表格,分别在table表格、tbody表格体、tr行、td单元格上绑定了click事件。假如我在td上执行了单击的操作,那么将会产生什么样的事件流呢?

第一种事件传递顺序是先触发最外层的table元素,然后向内传播,依次触发tbody、tr与td元素。

第二种事件传递顺序先触发由最内层的td元素,然后向外传播,依次触发tr、tbody与table元素。

第一种事件传递顺序对应的是捕获型事件流,第二种事件传递顺序对应的是冒泡型事件流。

一个完整的事件流实际包含了3个阶段:事件捕获阶段>事件目标阶段>事件冒泡阶段。上述两种类型的事件流实际对应其中的事件捕获阶段与事件冒泡阶段。

完整的事件处理阶段如图所示。

1. 事件捕获阶段

事件捕获阶段的主要表现是不具体的节点先接收事件,然后逐级向下传播,最具体的节点最后接收到事件。根据图中的指示就是Window > Document > html > body > table > tbody > tr > td。

2. 事件目标阶段:

事件目标阶段表示事件刚好传播到用户产生行为的元素上,可能是事件捕获的最后一个阶段,也可能是事件冒泡的第一个阶段。

3. 事件冒泡阶段:

事件冒泡阶段的主要表现是最具体的元素先接收事件,然后逐级向上传播,不具体的节点最后接收事件,根据图中的指示就是td> tr > tbody > table > body > html > Document >Window。

为了更直观地了解捕获型事件流和冒泡型事件流,我们会通过实际案例来进行测试。

由于table元素自身具有多层级结构,因此我们使用table元素来做演示,HTML代码如下所示。

html 复制代码
<table border="1">
    <tbody>
        <tr>
            <td>这是td的元素</td>
        </tr>
    </tbody>
</table>

然后依次给table、tbody、tr、td绑定click事件。

js 复制代码
<script>
    var table = document.querySelector('table');
    var tbody = document.querySelector('tbody');
    var tr = document.querySelector('tr');
    var td = document.querySelector('td');

    table.addEventListener('click', function () {
        console.log('table触发');
    });

    tbody.addEventListener('click', function () {
        console.log('tbody触发');
    });

    tr.addEventListener('click', function () {
        console.log('tr触发');
    });

    td.addEventListener('click', function () {
        console.log('td触发');
    });

</script>

① 使用addEventListener()函数绑定的事件在默认情况下,即第三个参数默认为false时,按照冒泡型事件流处理。当我们单击td单元格元素时,结果如下所示。

js 复制代码
td触发
tr触发
tbody触发
table触发

从td元素开始向外依次传播,经由tr、tbody,最终到达table元素。

② 使用addEventListener()函数同样可以很方便地创造出捕获型事件流,只需要将第三个参数设置为true即可。

相应的代码如下所示。

js 复制代码
table.addEventListener('click', function () {
    console.log('table触发');
}, true);

tbody.addEventListener('click', function () {
    console.log('tbody触发');
}, true);

tr.addEventListener('click', function () {
    console.log('tr触发');
}, true);

td.addEventListener('click', function () {
    console.log('td触发');
}, true);

当我们单击td元素时,结果如下所示。

js 复制代码
table触发
tbody触发
tr触发
td触发

从table元素开始向内依次传播,经由tbody、tr,最终到达td元素。以上的两种类型全部都是按照捕获性事件流或冒泡型事件流处理的,那么如果我们修改其中的任意两种为不同的模式以达到混合型事件流,结果会怎么样呢?

假如我们将table与tr设置为事件捕获类型,将tbody与td设置为事件冒泡类型,得到的代码如下。

js 复制代码
// 事件捕获
table.addEventListener('click', function () {
    console.log('table触发');
}, true);

// 事件冒泡
tbody.addEventListener('click', function () {
    console.log('tbody触发');
}, false);

// 事件捕获
tr.addEventListener('click', function () {
    console.log('tr触发');
}, true);

// 事件冒泡
td.addEventListener('click', function () {
    console.log('td触发');
}, false);

当我们单击td元素时,结果如下所示。

js 复制代码
table触发
tr触发
td触发
tbody触发

我们发现事件触发时,既没有按照元素由内向外的顺序,也没有按照元素由外向内的顺序。这是为什么呢?在本节一开始我们有讲到,完整的事件流是按照事件捕获阶段>事件目标阶段>事件冒泡阶段依次进行的。如果有元素绑定了捕获类型事件,则会优先于冒泡类型事件而先执行。整个事件流的实际执行过程分析如下。

  • 事件捕获阶段,从table元素开始,table元素绑定捕获类型事件,所以最先执行,输出"table触发"。
  • 事件捕获阶段,执行到tbody元素,但是tbody元素绑定的是冒泡类型事件,所以直接跳过,没有输出。
  • 事件捕获阶段,执行到tr元素,tr元素绑定了捕获类型事件,所以会执行,输出"tr触发"。
  • 事件目标阶段,执行到td元素,触发目标元素事件,不管是冒泡类型事件还是捕获类型事件,都会执行,输出"td触发"。
  • 事件冒泡阶段,执行到tr元素,tr元素绑定了捕获类型事件,所以直接跳过,没有输出。
  • 事件冒泡阶段,执行到tbody元素,tbody元素绑定了冒泡类型事件,所以会执行,输出"tbody触发"。
  • 事件冒泡阶段,执行到table元素,table元素绑定了捕获类型事件,所以直接跳过,没有输出。

针对以上的讲解,这里给大家出一道练习题,看看大家掌握得怎么样。如果对以上的过程理解得很透彻的话,相信大家可以很容易地找出答案。

对table元素和td元素绑定冒泡类型事件,对tbody元素和tr元素绑定捕获类型事件,在单击td元素的时候,结果会输出什么?

js 复制代码
// 事件冒泡
table.addEventListener('click', function () {
    console.log('table触发');
}, false);

// 事件捕获
tbody.addEventListener('click', function () {
    console.log('tbody触发');
}, true);

// 事件捕获
tr.addEventListener('click', function () {
    console.log('tr触发');
}, true);

// 事件冒泡
td.addEventListener('click', function () {
    console.log('td触发');
}, false);
复制代码
tbody触发
tr触发
td触发
table触发

5、事件处理程序

简单理解事件处理程序,就是响应某个事件的函数,例如onclick()函数、onload()函数就是响应单击、加载事件的函数,对应的是一段JavaScript的函数代码。

根据W3C DOM标准,事件处理程序分为DOM0、DOM2、DOM3这3种级别的事件处理程序。由于在DOM1中并没有定义事件的相关内容,因此没有所谓的DOM1级事件处理程序。

5.1、DOM0级事件处理程序

DOM0级事件处理程序是将一个函数赋值给一个事件处理属性,有两种表现形式。第一种是先通过JavaScript代码获取DOM元素,再将函数赋值给对应的事件属性。

js 复制代码
var btn = document.getElementById("btn"); 
btn.onclick = function(){}

第二种是直接在html中设置对应事件属性的值,值有两种表现形式,一种是执行的函数体,另一种是函数名,然后在script标签中定义该函数。

html 复制代码
<button onclick="alert('面试厅');">单击</button>
<button onclick="clickFn()">单击</button>
<script>
    function clickFn() {
        alert('面试厅');
    }
</script>

以上两种DOM0级事件处理程序同时存在时,第一种在JavaScript中定义的事件处理程序会覆盖掉后面在html标签中定义的事件处理程序。

需要注意的是,DOM0级事件处理程序只支持事件冒泡阶段。DOM0级事件处理程序的优缺点如下。

  • 优点:简单且可以跨浏览器。
  • 缺点:一个事件处理程序只能绑定一个函数。

例如,我们分别在HTML和JavaScript中使用两种方法绑定onclick事件处理程序。

html 复制代码
<button class="btn" id="btn" onclick="doClick()">click me</button>
js 复制代码
var btn = document.getElementById("btn");
btn.onclick = function(){
    console.log('123');
};
function doClick() {
    console.log('456');
}

由于DOM0级事件只能绑定一个函数,而且在JavaScript中绑定事件处理程序的优先级高于在HTML中定义的事件处理程序,因此最后的结果是输出"123"​。

如需删除元素绑定的事件,只需要将对应的事件处理程序设置为null即可。

js 复制代码
btn.onclick = null;

5.2、DOM2级事件处理程序

在DOM2级事件处理程序中,当事件发生在节点时,目标元素的事件处理函数就会被触发,而且目标元素的每个祖先节点也会按照事件流顺序触发对应的事件处理程序。DOM2级事件处理方式规定了添加事件处理程序和删除事件处理程序的方法。

针对DOM2级事件处理程序,不同的浏览器厂商制定了不同的实现方式,主要分为IE浏览器和非IE浏览器。

在IE10及以下版本中,只支持事件冒泡阶段。在IE11中同时支持事件捕获阶段与事件冒泡阶段。在IE10及以下版本中,可以通过attachEvent()函数添加事件处理程序,通过detachEvent()函数删除事件处理程序。

js 复制代码
element.attachEvent("on"+ eventName, handler);         //添加事件处理程序
element.detachEvent("on"+ eventName, handler);         //删除事件处理程序

在IE11及其他非IE浏览器中,同时支持事件捕获和事件冒泡两个阶段,可以通过addEventListener()函数添加事件处理程序,通过removeEventListener()函数删除事件处理程序。

js 复制代码
addEventListener(eventName, handler, useCapture);       //添加事件处理程序
removeEventListener(eventName, handler, useCapture);  //删除事件处理程序

其中的useCapture参数表示是否支持事件捕获,true表示支持事件捕获,false表示支持事件冒泡,默认状态为false。

既然DOM2级事件处理程序存在两种实现方式,那么它们之间有没有共同点和不同点呢?

1. 共同点:

① 在DOM2级事件处理程序中,不管是IE浏览器还是非IE浏览器都支持对同一个事件绑定多个处理函数。

js 复制代码
var handler1 = function (){}
var handler2 = function (){}
---------------IE10及以下------------------
btn.attachEvent('onclick', handler1);
btn.attachEvent('onclick', handler2);
  
---------------IE11及非IE-----------------
btn.addEventListener('click', handler1);
btn.addEventListener('click', handler2);

如下面的实例所示,我们在div上同时绑定了两个click函数。

js 复制代码
<div id="wrap">单击我触发事件</div>

var wrap = document.getElementById('wrap');

wrap.addEventListener('click', function() {
    console.log('123');
}, false);

wrap.addEventListener('click', function () {
    console.log('456');
}, false);

当我们单击div时,会先后输出"123"和"456"​。

② 在需要删除绑定的事件时,不能删除匿名函数,因为添加和删除的必须是同一个函数。下面这种同时绑定和取消handler()函数的情况,可以删除掉绑定的事件。

js 复制代码
var wrap = document.getElementById('wrap');

var handler = function () {
    console.log('789');
};

// 第一种方式绑定和取消的是同一个函数,因此可以取消绑定的事件
wrap.addEventListener('click', handler, false);
wrap.removeEventListener('click', handler);

而如果采用下面这种方式,则无法取消绑定的事件,因为它们使用的都是匿名函数的形式,绑定与取消的函数并不是同一个。

js 复制代码
wrap.addEventListener('click', function () {
    console.log('123');
}, false);

wrap.removeEventListener('click', function () {});

2. 不同点:

① 在IE浏览器中,使用attachEvent()函数为同一个事件添加多个事件处理函数时,会按照添加的相反顺序执行。

假如我们在一个button元素上使用attachEvent()函数先后绑定了两个onclick事件处理程序,具体代码如下所示。

js 复制代码
var btn=document.getElementById("mybtn");
btn.attachEvent("onclick",function(){
    console.log("clicked");
});
btn.attachEvent("onclick",function(){
    console.log("hello world!");
});

在单击button按钮时,会先输出"hello world"​,后输出"clicked"​。

② 在IE浏览器下,使用attachEvent()函数添加的事件处理程序会在全局作用域中运行,因此this指向全局作用域window。在非IE浏览器下,使用addEventListener()函数添加的事件处理程序在指定的元素内部执行,因此this指向绑定的元素。

js 复制代码
<button id="mybtn">单击</button>
<script>
  var btn = document.getElementById("mybtn");
  // IE浏览器
  btn.attachEvent("onclick", function () {
      alert(this); // 指向window
});
  // 非IE浏览器
    btn.addEventListener("click", function () {
      alert(this); // 指向绑定的元素
});
  </script>

因为浏览器的差异性,我们需要使用不同的方法来实现DOM2级事件处理程序。如果我们想要针对不同的浏览器做兼容性处理,该如何实现呢?

以下是一段针对不同浏览器所做的封装处理代码。

js 复制代码
var EventUtil = {
    addEventHandler: function (element, type, handler) {
        if (element.addEventListener) {
            element.addEventListener(type, handler);
        } else if (element.attachEvent){
            element.attachEvent("on" + type, handler);
        } else {
            element["on" + type] = handler;
        }
    },
    removeEventHandler: function (element, type, handler) {
        if (element.addEventListener) {
            element.removeEventListener(type, handler);
        } else if (element.detachEvent){
            element.detachEvent("on" + type, handler);
        } else {
            element["on"+type] = null;
        }
    }
}

EventUtil是与事件有关的所有兼容性处理方案中的工具类,后续还有多种处理函数都会依次添加到EventUtil中。

5.3、DOM3级事件处理程序

DOM3级事件处理程序是在DOM2级事件的基础上重新定义了事件,也添加了一些新的事件。最重要的区别在于DOM3级事件处理程序允许自定义事件,自定义事件由createEvent("CustomEvent")函数创建,返回的对象有一个initCustomEvent()函数,通过传递对应的参数可以自定义事件。

函数可以接收以下4个参数。

  • type:字符串、触发的事件类型、自定义,例如"keyDown""selectedChange"。
  • bubble(布尔值):表示事件是否可以冒泡。
  • cancelable(布尔值):表示事件是否可以取消。
  • detail(对象):任意值,保存在event对象的detail属性中。

创建完成的自定义事件,可以通过dispatchEvent()函数去手动触发,触发自定义事件的元素需要和绑定自定义事件的元素为同一个元素。

接下来我们通过实例来看看自定义事件的处理方式。我们需要实现的场景是:在页面初始化时创建一个自定义事件myEvent,页面上有个div监听这个自定义事件myEvent,同时有一个button按钮绑定了单击事件;当我们单击button时,触发自定义事件,由div监听到,然后做对应的处理。

上述场景可以分为3步去实现。

  • 创建自定义事件。
  • 监听自定义事件。
  • 触发自定义事件。

首先我们来看看HTML代码,包含一个div元素和一个button元素。

html 复制代码
<div id="watchDiv">监听自定义事件的div元素</div>
<button id="btn">单击触发自定义事件</button>

然后是重点的JavaScript实现。

  • 创建自定义事件。

通过立即执行函数创建一个自定义事件。该自定义事件支持冒泡,而且会携带参数detailData。

在创建自定义事件之前,需要判断浏览器是否支持DOM3级事件处理程序。可以通过判断下面代码的返回值来确认,如果返回值为"true"​,则表示浏览器支持;如果返回值为"false"​,则表示浏览器不支持。

js 复制代码
document.implementation.hasFeature('CustomEvents', '3.0');

得到的代码如下。

js 复制代码
var customEvent;
// 创建自定义事件
(function () {
    if (document.implementation.hasFeature('CustomEvents', '3.0')) {
        var detailData = {name: 'kingx'};
        customEvent = document.createEvent('CustomEvent');
        customEvent.initCustomEvent('myEvent', true, false, detailData);
    }
})();
  • 监听自定义事件。

通过addEventListener()函数监听自定义的myEvent事件。

js 复制代码
// 获取元素
var div = document.querySelector('#watchDiv');
// 监听myEvent事件
div.addEventListener('myEvent', function (e) {
    console.log('div监听到自定义事件的执行, 携带的参数为: ', e.detail);
});
  • 触发自定义事件。

我们将触发自定义事件的入口放在button上,当单击button时会通过dispatchEvent()函数触发myEvent事件。

js 复制代码
// 获取元素
var btn = document.querySelector('#btn');
// 绑定click事件,触发自定义事件
btn.addEventListener('click', function () {
    div.dispatchEvent(customEvent);
});

运行以上代码,当我们单击button按钮后,会看到如下结果。

html 复制代码
div监听到自定义事件的执行,携带的参数为:{name: "kingx"}

该结果表明,在div上监听的自定义事件得到了触发,传递的detailData参数也得以接收。

自定义事件支持事件冒泡机制,可以在初始化自定义事件的initCustomEvent()函数中通过第二个参数来设置事件是否可以冒泡,上述例子中自定义的myEvent事件是支持冒泡的。

沿用上面的例子,我们在document上增加了对自定义的myEvent事件的监听。

js 复制代码
document.addEventListener('myEvent', function () {
    console.log('document监听到自定义事件的执行');
});

当我们单击button按钮时,得到的结果如下所示。

js 复制代码
div监听到自定义事件的执行,携带的参数为:{name: "kingx"}
document监听到自定义事件的执行

通过结果可以看出,由于自定义的myEvent事件是支持事件冒泡的,所以div和document都会监听到myEvent事件的执行,输出对应的结果。

而当我们将myEvent事件设置为不支持事件冒泡时,其代码如下。

js 复制代码
if (document.implementation.hasFeature('CustomEvents', '3.0')) {
    var detailData = {name: 'kingx'};
    customEvent = document.createEvent('CustomEvent');
    // 第二个参数设置为false,表示不支持事件冒泡
    customEvent.initCustomEvent('myEvent', false, false, detailData);
}

再去单击button按钮,得到的结果如下所示。

js 复制代码
div监听到自定义事件的执行,携带的参数为:{name: "kingx"}

从结果可以看出,document上监听的myEvent事件并未触发,事件冒泡被阻止了。

6、Event对象

事件在浏览器中是以Event对象的形式存在的,每触发一个事件,就会产生一个Event对象。该对象包含所有与事件相关的信息,包括事件的元素、事件的类型及其他与特定事件相关的信息。

Event对象有一系列的属性和函数,但是考虑到它们的使用频率的情况,我们不会一一介绍,我们会选择其中比较重要的特性通过实例进行讲解。

因为Event对象在不同浏览器中的实现是有差异性的,这里我们事先准备好不同的浏览器进行测试,浏览器类型与版本号分别是:Safari 10.0.3版本、Firefox 61.0.1版本、Chrome 68.0.3440版本。

在文章中出现的浏览器名称将会对应以上指定的版本,例如文章中出现的"在Chrome浏览器中"字样,指的就是在Chrome 68.0.3440版本的浏览器中。

6.1、获取Event对象

在给元素绑定特定的事件处理程序时,可以获取到Event对象,但是考虑到不同浏览器的差异性,获取Event对象的方式也不同。获取Event对象的方式有以下两种。

  • 在事件处理程序中,Event对象会作为参数传入,参数名为event。
  • 在事件处理程序中,通过window.event属性获取Event对象。

我们在同一个事件处理程序中,可以使用上述两种方式获取event对象并输出。

js 复制代码
var btn = document.querySelector('#btn');

btn.addEventListener('click', function (event) {
    // 方式1:event作为参数传入
    console.log(event);
    // 方式2:通过window.event获取
    var winEvent = window.event;
    console.log(winEvent);
    // 判断两种方式获取的event是否相同
    console.log(event == winEvent);
});

分别在Chrome、Firefox和Safari浏览器中运行,并单击id为btn的按钮。在Chrome浏览器中运行时,得到的结果如下所示。

js 复制代码
MouseEvent {isTrusted: true, screenX: 119, screenY: 321, ...}
MouseEvent {isTrusted: true, screenX: 119, screenY: 321, ...}
true

在Firefox浏览器中运行时,得到的结果如下所示。

js 复制代码
click { target: button#btn2, buttons: 0, clientX: 145, ...}
undefined
false

在Safari浏览器中运行时,得到的结果如下所示。

js 复制代码
MouseEvent {isTrusted: true, screenX: 119, screenY: 321, ...}
MouseEvent {isTrusted: true, screenX: 119, screenY: 321, ...}
true

从结果可以看出,不同的浏览器的表现还是有差异性的。Chrome浏览器和Safari浏览器同时支持两种方式获取event对象,而Firefox浏览器只支持这种将event作为参数传入的方式。

在获取事件对象时,为了支持不同浏览器,我们可以通过以下代码来实现兼容。

js 复制代码
var EventUtil = {
    // 获取事件对象
    getEvent: function (event) {
        return event || window.event;
    }
};

6.2、获取事件的目标元素

在事件处理程序中,我们可能经常需要获取事件的目标元素,以便对目标元素做相应的处理。

在IE浏览器中,event对象使用srcElement属性来表示事件的目标元素;而在非IE浏览器中,event对象使用target属性来表示事件的目标元素,为了提供与IE浏览器下event对象相同的特性,某些非IE浏览器也支持srcElement属性。

同理,我们在同一个事件处理程序中使用上述两种属性来获取事件的目标元素。

js 复制代码
btn.addEventListener('click', function (event) {
    // 获取event对象
    var event = EventUtil.getEvent(event);
    // 使用两种属性获取事件的目标元素
    var NoIETarget = event.target;
    var IETarget = event.srcElement;
    console.log(NoIETarget);
    console.log(IETarget);
});

分别在Chrome、Firefox和Safari浏览器中运行,并单击id为btn的按钮。在Chrome浏览器中运行时,得到的结果如下所示。

html 复制代码
<button id="btn">单击</button>
<button id="btn">单击</button>

在Firefox浏览器中运行时,得到的结果如下所示。

html 复制代码
<button id="btn">单击</butto
undefined

在Safari浏览器中运行时,得到的结果如下所示。

html 复制代码
<button id="btn">单击</button>
<button id="btn">单击</button>

从结果可以看出,Chrome浏览器和Safari浏览器同时支持两种属性来获取事件目标元素,而Firefox浏览器只支持event.target属性来获取事件目标元素。在获取事件目标元素时,为了支持不同的浏览器,我们可以通过以下代码来做兼容。

js 复制代码
var EventUtil = {
    ... 
    // 获取事件目标元素
    getTarget: function (event) {
        return event.target || event.srcElement;
    }
};

6.3、target属性与currentTarget属性

在Event对象中有两个属性总是会引起大家的困扰,那就是target属性和currentTarget属性。两者都可以表示事件的目标元素,但是在事件流中两者却有不同的意义。首先我们简单地介绍下两者的区别,然后通过实例来看它们在事件流中的表现。

  • target属性在事件目标阶段,理解为真实操作的目标元素。·
  • currentTarget属性在事件捕获、事件目标、事件冒泡这3个阶段,理解为当前事件流所处的某个阶段对应的目标元素。

沿用之前的实例,我们使用table元素,分别在table、tbody、tr、td元素上绑定事件捕获阶段和事件冒泡阶段的click事件。click事件中输出对应的target属性和currentTarget属性值,为了更直观地看出结果,我们取target对象的tagName属性来获取标签名。

js 复制代码
// 获取target属性和currentTarget属性的元素标签名
function getTargetAndCurrentTarget(event, stage) {
    var event = EventUtil.getEvent(event);
    var stageStr;
    if (stage === 'bubble') {
        stageStr = '事件冒泡阶段';
    } else if(stage === 'capture'){
        stageStr = '事件捕获阶段';
    } else {
        stageStr = '事件目标阶段';
    }
    console.log(stageStr,
            'target:' + event.target.tagName.toLowerCase(),
            'currentTarget: ' + event.currentTarget.tagName.toLowerCase());
}

// 事件捕获
table.addEventListener('click', function (event) {
    getTargetAndCurrentTarget(event, 'capture');
}, true);

// 事件捕获
tbody.addEventListener('click', function (event) {
    getTargetAndCurrentTarget(event, 'capture');
}, true);

// 事件捕获
tr.addEventListener('click', function (event) {
    getTargetAndCurrentTarget(event, 'capture');
}, true);

// 事件捕获
td.addEventListener('click', function (event) {
    getTargetAndCurrentTarget(event, 'target');
}, true);

// 事件冒泡
table.addEventListener('click', function (event) {
    getTargetAndCurrentTarget(event, 'bubble');
}, false);

// 事件冒泡
tbody.addEventListener('click', function (event) {
    getTargetAndCurrentTarget(event, 'bubble');
}, false);

// 事件冒泡
tr.addEventListener('click', function (event) {
    getTargetAndCurrentTarget(event, 'bubble');
}, false);

// 事件冒泡
td.addEventListener('click', function (event) {
    getTargetAndCurrentTarget(event, 'target');
}, false);

当我们单击td元素时,会触发整个事件流,得到的结果如下所示。

js 复制代码
事件捕获阶段 target:td currentTarget: table
事件捕获阶段 target:td currentTarget: tbody
事件捕获阶段 target:td currentTarget: tr
事件目标阶段 target:td currentTarget: td
事件目标阶段 target:td currentTarget: td
事件冒泡阶段 target:td currentTarget: tr
事件冒泡阶段 target:td currentTarget: tbody
事件冒泡阶段 target:td currentTarget: table

为什么会得出这个结果呢?

在事件流的任何阶段,target属性始终指向的是实际操作的元素。因为我们是在td元素上进行的单击操作,所以target属性对应的是td。

在事件流的事件捕获阶段或者事件冒泡阶段,currentTarget指向的是事件流所处的某个特定阶段对应的元素。在该实例中,事件捕获阶段元素的流转顺序为table>tbody>tr,事件冒泡阶段元素的流转顺序为tr>tbody>table。

在事件目标阶段,currentTarget属性指向的也是实际操作的元素,即td。因此只有在事件目标阶段,target属性和currentTarget属性才指向同一个元素。

为了巩固大家对这两个属性的理解,这里出一道简单的练习题。页面上有一个ul-li标签,同时在ul和第一个li上绑定事件冒泡阶段的click事件,所有代码如下所示。

js 复制代码
<ul id="ul">
    <li id="li">第一个元素</li>
    <li>第二个元素</li>
</ul>

<script>
    var ul = document.querySelector('#ul');
    var li = document.querySelector('#li');

    ul.addEventListener('click', function (event) {
        getTargetAndCurrentTarget(event, 'bubble');
    });

    li.addEventListener('click', function (event) {
        getTargetAndCurrentTarget(event, 'bubble');
    });

</script>

提问:当单击li的时候,会输出什么样的结果?最终的结果如下所示,是否与你想的一致?

js 复制代码
事件冒泡阶段 target:li currentTarget: li
事件冒泡阶段 target:li currentTarget: ul

6.4、阻止事件冒泡

事件冒泡对于DOM操作有很大的帮助,在后面5.7节中讲到的事件委托能够体现出来。

但有时我们并不想要事件进行冒泡,例如下面所述的场景。有一个表示学生基础信息的容器ul,每个li元素都表示一个学生的基本信息,单击li元素会改变li的背景色以表示选中的标识。在每个li元素内部会有一个表示删除的button按钮,单击button按钮则会提示是否删除,单击确定则会删除该元素。为了单纯地讲解关于阻止事件冒泡的操作,实际的代码中我们省略掉了一些操作,方便大家了解重点。

html 复制代码
<ul>
    <li>
        <p>姓名:小明</p>
        <p>学号:20180101</p>
        <button class="btn btn-default" id="btn">删除</button>
    </li>
</ul>

<script>

    var li = document.querySelector('li');
    var btn = document.querySelector('#btn');

    li.addEventListener('click', function (event) {
        // 真实操作,使用console来代替
        console.log('单击了li,做对应的处理');
    });

    btn.addEventListener('click', function (event) {
        // 真实操作,使用console来代替
        console.log('单击了button,做对应的处理');
    });

</script>

上面代码在浏览器中执行后,当我们单击button按钮时,运行的结果如下所示。

js 复制代码
单击了button,做对应的处理
单击了li,做对应的处理

由于事件冒泡的存在,在单击button按钮时,事件同样会冒泡至父元素li上,因此两个click事件都会被触发。但这并不是我们想要的结果,我们所期望的只有在单击li时,才会触发li的操作;只有在单击button时,才触发button的操作。这该怎么去做呢?

我们只需要阻止事件冒泡就可以了,即在button按钮的click事件中调用event.stopPropagation()函数。那么在单击button按钮时,事件就只会在事件目标阶段执行,而不会向上继续冒泡至父元素li中,从而达到目的。相应的button按钮的click事件代码的更改如下,增加了对阻止事件冒泡的处理。

js 复制代码
btn.addEventListener('click', function (event) {
    var event = EventUtil.getEvent(event);
    // 阻止事件冒泡
    event.stopPropagation();
    // 真实操作,使用console来代替
    console.log('单击了button,做对应的处理');
});

这个时候再去单击button按钮,就会得到下面的结果。

js 复制代码
单击了button,做对应的处理

通过结果可以看出,只触发了button按钮的click事件,并未触发父元素li的click事件;而在单击父元素li时,依然可以触发li的click事件,输出对应的结果。此外,细心的同学可能会发现,在event对象中还存在一个stopImmediatePropagation()函数,从函数名可以看出它的作用也是用于阻止事件冒泡的,但是它和stopPropagation()函数有什么区别呢?

两者的区别主要体现在同一事件绑定多个事件处理程序的情况下。· stopPropagation()函数仅会阻止事件冒泡,其他事件处理程序仍然可以调用。· stopImmediatePropagation()函数不仅会阻止冒泡,也会阻止其他事件处理程序的调用。我们沿用上面实例的代码,对button按钮的click事件增加3个事件处理程序,在第二个事件处理程序中使用stopPropagation()函数来阻止事件冒泡。

js 复制代码
var li = document.querySelector('li');
var btn = document.querySelector('#btn');
li.addEventListener('click', function (event) {
    // 真实操作,使用console来代替
    console.log('单击了li,做对应的处理');
});

// 第一个事件处理程序
btn.addEventListener('click', function (event) {
    // 真实操作,使用console来代替
    console.log('button的第一个事件处理程序,做对应的处理');
});

// 第二个事件处理程序
btn.addEventListener('click', function (event) {
    var event = EventUtil.getEvent(event);
    // 阻止事件冒泡
    event.stopPropagation();
    // 真实操作,使用console来代替
    console.log('button的第二个事件处理程序,做对应的处理');
    });

// 第三个事件处理程序
btn.addEventListener('click', function (event) {
    // 真实操作,使用console来代替
    console.log('button的第三个事件处理程序,做对应的处理');
});

当我们单击button按钮后,得到的结果如下所示。

js 复制代码
button的第一个事件处理程序,做对应的处理
button的第二个事件处理程序,做对应的处理
button的第三个事件处理程序,做对应的处理

从结果可以看出,事件冒泡被阻止,li上的事件处理程序并未触发;绑定的3个事件处理程序都被触发执行。

当我们将第二个事件处理程序中的stopPropagation()函数替换为stopImmediatePropagation()函数,又会得到什么样的结果呢?

js 复制代码
// 第二个事件处理程序
btn.addEventListener('click', function (event) {
    var event = EventUtil.getEvent(event);
    // 阻止事件冒泡
    event.stopImmediatePropagation();
    // 真实操作,使用console来代替
    console.log('btn的第二个事件处理程序,做对应的处理');
});

当我们单击button按钮后,得到的结果如下所示。

js 复制代码
button的第一个事件处理程序,做对应的处理
button的第二个事件处理程序,做对应的处理

从结果可以看出,事件冒泡被阻止,li上的事件处理程序并未触发;只有第一个和第二个事件处理程序被触发执行,而第三个事件处理程序并未执行。

6.5、阻止默认行为

在众多的HTML标签中,有一些标签是具有默认行为的,这里简单地列举3个。

  • a标签,在单击后默认行为会跳转至href属性指定的链接中。
  • 复选框checkbox,在单击后默认行为是选中的效果。
  • 输入框text,在获取焦点后,键盘输入的值会对应展示到text输入框中。

在一般情况下我们是允许标签的默认行为的,就像用户的正常操作,但是在某些时候我们是需要阻止这些标签的默认行为的,同样使用上述3种场景作为说明。

  • a标签,假如a标签上显示的文案不符合预期,我们在单击a标签时将不会跳转至对应的链接中去。
  • 复选框checkbox,假如已选中的复选框在单击的时候不会被取消,依然是选中的状态。
  • 输入框text,假如我们限制用户输入的值只能是数字和大小写字母,其他的值不允许输入。

那么该如何编写代码来阻止元素的默认行为呢?很简单,就是通过event.preventDefault()函数去实现。为了更详细地说明阻止默认行为的操作,我们选择输入框来做具体说明。场景描述:限制用户输入的值只能是数字和大小写字母,其他的值则不能输入,如输入其他值则给出提示信息,提示信息在两秒后消失。

在本实例中,因为涉及键盘输入,所以我们需要监听keypress事件,通过兼容性来处理获取当前按键的值,然后判断输入的值是否合法,从而控制键盘输入的行为。在这里我们需要掌握一些关于键盘按键值的知识点,其实就是键盘的每个键有对应的Unicode编码。本例需要获取的数字和字母的Unicode编码范围如下所示。

  • 数字的Unicode编码范围是48~57。
  • 大写字母A~Z的Unicode编码范围是65~90。
  • 小写字母a~z的Unicode编码范围是97~122。

在了解了需要获取的键的Unicode编码范围后,接下来要做的就是获取这些Unicode编码,并将其用在程序中作判断。同样是因为浏览器的兼容性问题,Event对象提供了多种不同的属性来获取键的Unicode编码,分别是event.keyCode、event.charCode和event.which。我们可以通过以下方式来做兼容性处理。

js 复制代码
var charCode = event.keyCode || event.which || event.charCode;

根据以上分析,最终所得的代码如下所示。

js 复制代码
<input type="text" id="text">
<div id="tip"></div>

<script>
    var text = document.querySelector('#text');
    var tip = document.querySelector('#tip');
    text.addEventListener('keypress', function (event) {
        var charCode = event.keyCode || event.which || event.charCode;
        // 满足输入数字
        var numberFlag = charCode <= 57 && charCode >= 48;
        // 满足输入大写字母
        var lowerFlag = charCode <= 90 && charCode >= 65;
        // 满足输入小写字母
        var supperFlag = charCode <= 122 && charCode >= 97;

        if (!numberFlag && !lowerFlag && !supperFlag) {
            // 阻止默认行为,不允许输入
            event.preventDefault();
            tip.innerText = '只允许输入数字和大小写字母';
        }
        // 设置定时器,清空提示语
        setTimeout(function () {
            tip.innerText = '';
        }, 2000);
    });
</script>

运行上面的代码,在页面出现的text文本框中进行输入,输入0~9或大小写字母时可以正常输入,而输入下画线(_)​、加号(+)等字符时是无法输入的,同时文本框下会出现"只允许输入数字和大小写字母"的提示。

7、事件委托

事件委托是利用事件冒泡原理,管理某一类型的所有事件,利用父元素来代表子元素的某一类型事件的处理方式。这单从字面意思会很难理解,本节将通过两种比较常见的场景来进行分析,一种是已有元素的事件绑定,另一种是新创建元素的事件绑定。

7.1、已有元素的事件绑定

场景:假如页面上有一个ul标签,里面包含1000个li子标签,我们需要在单击每个li时,输出li中的文本内容。遇到这样的场景时,很多人的第一想法就是给每个li标签绑定一个click事件,在click事件中输出li标签的文本内容,以下是一些简单易懂的实现方法。

1. HTML代码:

HTML代码很简单,就是一个包含很多li标签的ul标签,后面过多的代码使用省略号代替。

js 复制代码
<ul>
    <li>文本1</li>
    <li>文本2</li>
    <li>文本3</li>
    <li>文本4</li>
    <li>文本5</li>
    <li>文本6</li>
    <li>文本7</li>
    <li>文本8</li>
    <li>文本9</li>
    ...
</ul>

2. JavaScript代码:

在获取所有的li标签后,对其进行遍历,在遍历时添加click事件处理程序。

js 复制代码
<script>
    // 1.获取所有的li标签
    var children = document.querySelectorAll('li');
    // 2.遍历添加click事件处理程序
    for (var i = 0; i < children.length; i++) {
        children[i].addEventListener('click', function () {
            console.log(this.innerText);
        });
    }
</script>

当我们单击li标签时,会对应地输出li标签上的内容,如下所示。

js 复制代码
文本1
文本6
文本9
文本7

采用上述的方法对浏览器的性能是一个很大的挑战,主要包含以下两方面原因。

  • 事件处理程序过多导致页面交互时间过长。

假如有1000个li元素,则需要绑定1000个事件处理程序,而事件处理程序需要不断地与DOM节点进行交互,因此引起浏览器重绘和重排的次数也会增多,从而会延长页面交互时间。

  • 事件处理程序过多导致内存占用过多。

在JavaScript中,一个事件处理程序其实就是一个函数对象,会占用一定的内存空间。假如页面有10000个li标签,则会有10000个函数对象,占用的内存空间会急剧上升,从而影响浏览器的性能。

那么遇到这个问题时,有什么好的解决办法呢?答案就是利用事件委托机制。事件委托机制的主要思想是将事件绑定至父元素上,然后利用事件冒泡原理,当事件进入冒泡阶段时,通过绑定在父元素上的事件对象来判断当前事件流正在进行的元素。如果和期望的元素相同,则执行相应的事件代码。根据以上的分析,我们可以按步骤依次得到以下代码。

js 复制代码
// 1.获取父元素
var parent = document.querySelector('ul');
// 2.父元素绑定事件
parent.addEventListener('click', function (event) {
    // 3.获取事件对象
    var event = EventUtil.getEvent(event);
    // 4.获取目标元素
    var target = EventUtil.getTarget(event);
    // 5.判断当前事件流所处的元素
    if (target.nodeName.toLowerCase() === 'li') {
          // 6.与目标元素相同,做对应的处理
        console.log(target.innerText);
    }
});

运行上面的代码,当我们单击li标签时,会得到与前面方法相同的输出。通过上面的代码可以看出,事件是绑定在父元素ul上的,不管子元素li有多少个,也不会影响到页面中事件处理程序的个数,因此可以极大地提高浏览器的性能。在上面的场景中,同一个ul下的所有li所做的操作都是一样的,使用事件委托即可处理。那么如果针对不同的元素所做的处理不一样,事件委托能否处理呢?答案当然是可以的,我们可以假定如下所述的场景。在页面上有4个button按钮,分别表示增加、删除、修改、查询这4个功能。每个按钮绑定相同的click事件处理程序,但是具体的行为不同。在这4个按钮触发click事件后,分别输出"新增"​"删除"​"修改"​"查询"等文字。

html 复制代码
<div id="box">
    <input type="button" id="add" value="新增" />
    <input type="button" id="remove" value="删除" />
    <input type="button" id="update" value="修改" />
    <input type="button" id="search" value="查询" />
</div>
js 复制代码
<script>
    var add = document.querySelector('#add');
    var remove = document.querySelector('#remove');
    var update = document.querySelector('#update');
    var search = document.querySelector('#search');
    // 新增按钮绑定事件
    add.addEventListener('click', function () {
        console.log('新增');
    });
    // 删除按钮绑定事件
    remove.addEventListener('click', function () {
            console.log('删除');
    });
    // 修改按钮绑定事件
    update.addEventListener('click', function () {
        console.log('修改');
    });
    // 查询按钮绑定事件
    search.addEventListener('click', function () {
        console.log('查询');
    });

</script>

和第一个实例一样,对于不同的按钮都需要绑定一个click事件处理程序,这样在性能上会存在一定的影响。那么使用事件委托可以怎么做呢?主要遵循以下3步。

  • 获取button的父元素,在父元素上绑定click事件处理程序。·
  • 获取event事件对象,紧接着通过event事件对象获取到目标元素。· 获取目标元素的id值,与HTML元素中各个button的id进行比较,输出对应的文字。
js 复制代码
// 1.获取父元素,并绑定事件处理程序
var parent = document.querySelector('#parent');
parent.addEventListener('click', function (event) {
    // 2.获取event和target
    var event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
    // 3.判断id属性,输出对应的文字
    switch (target.id) {
        case 'add':
            console.log('新增');
            break;
        case 'remove':
            console.log('删除');
            break;
        case 'update':
            console.log('修改');
            break;
        case 'search':
            console.log('查询');
            break;
    }
});

使用事件委托可以同样很好地解决了不同元素不同处理的情况,从而证明事件委托对于元素事件的处理,尤其是处理多个元素时具有天然优势。

7.2、新创建元素的事件绑定

场景:假如页面上有一个ul标签,里面包含9个li子标签,我们需要在单击每个li时,输出li中的文本内容;在页面上有一个button按钮,单击button按钮会创建一个新的li元素,单击新创建的li元素,输出它的文本内容。根据上面的场景描述,我们可以通过以下两种方法来实现。

7.2.1、手动绑定方法

首先是和5.7.1中相同的代码,由于逻辑是相同的,这里就不赘述,直接给出代码。

html 复制代码
<ul>
    <li>文本1</li>
    <li>文本2</li>
    <li>文本3</li>
    <li>文本4</li>
    <li>文本5</li>
    <li>文本6</li>
    <li>文本7</li>
    <li>文本8</li>
    <li>文本9</li>
</ul>

// 1.获取所有的li标签
var children = document.querySelectorAll('li');
// 2.遍历添加click事件处理程序
for (var i = 0; i < children.length; i++) {
    children[i].addEventListener('click', function () {
        console.log(this.innerText);
    });
}

然后在页面上添加一个button按钮,用于新增一个li元素。

js 复制代码
<button id="add">新增</button>
var ul = document.querySelector('ul');
var add = document.querySelector('#add');
add.addEventListener('click', function () {
    // 创建新的li元素
    var newLi = document.createElement('li');
    var newText = document.createTextNode('文本10');
    newLi.appendChild(newText);
    // 添加至父元素ul中
    ul.appendChild(newLi);
});

当我们单击新增按钮时,会发现页面上新增了一个内容为"文本10"的li元素。当我们单击这个li元素时,会在控制台输出"文本10"吗?我们在浏览器中验证后会发现,控制台中没有输出任何内容。这是为什么呢?因为我们通过querySelectorAll()函数获取到的li元素虽然会实时感知到数量的变化,但并不会实时增加对事件的绑定。如果需要新元素也具有相同的事件,则需要手动调用事件绑定的代码。解决方案如下。

解决方案如下。

① 将遍历添加click事件处理程序代码封装成一个函数。

js 复制代码
// 遍历添加click事件处理程序
function bindEvent() {
    for (var i = 0; i < children.length; i++) {
        children[i].addEventListener('click', function () {
            console.log(this.innerText);
        });
    }
}

② 在添加完新元素后,重新调用一次①中封装的函数。

js 复制代码
add.addEventListener('click', function(){
	var newLi = document.createElement('li');
	var newText = document.createTextNode('文本10');
	newLi.appendChild(newText);
	ul.appendChild(newLi);
	//重新添加事件处理程序
	bindEvent();
});

但是,通过上面的分析我们发现,每次在新增一个元素后都需要手动绑定事件处理程序,这样的操作是很烦琐的,而且随着绑定的事件处理程序越来越多,性能也将受到影响。

那么,我们有没有什么更好的方法呢?答案就是使用事件委托机制。

7.2.2、事件委托方法

使用事件委托机制,我们可以更加方便快捷地实现新创建元素的事件绑定。由于事件委托机制是利用的事件冒泡机制,即使在元素自身没有绑定事件的情况下,事件仍然会冒泡到父元素中,因此对于新增的元素,只要处理事件流就可以触发其事件。针对上述问题的描述,我们需要做的就是使用事件委托机制编写代码。

js 复制代码
<script>
    // 1.获取父元素
    var parent = document.querySelector('ul');
    // 2.父元素绑定事件
    parent.addEventListener('click', function (event) {
        // 3.获取事件对象
        var event = EventUtil.getEvent(event);
        // 4.获取目标元素
        var target = EventUtil.getTarget(event);
        // 5.判断当前事件流所处的元素
        if (target.nodeName.toLowerCase() === 'li') {
            // 6.与目标元素相同,做对应的处理
            console.log(target.innerText);
        }
    });

</script>

新增按钮的事件不变,和方法1中的一样,这里就不赘述。当我们在浏览器中运行可以发现,新增的li元素在单击后,会在控制台输出"文本10"​,这就代表使用事件委托机制方便快捷地解决了这个问题。

相关推荐
紫_龙2 小时前
最新版vue3+TypeScript开发入门到实战教程之路由详解
javascript·typescript·智能路由器
胡楚昊2 小时前
BUUCTF RE 看心情写(2)
开发语言·c#
2401_879693872 小时前
C++跨平台开发实战
开发语言·c++·算法
皙然2 小时前
深度解析 “池化思想”:从设计模式到 Java 技术栈的落地与实践
java·开发语言·设计模式
旺仔.2912 小时前
C++ String 详解
开发语言·c++·算法
智算菩萨2 小时前
OpenCV+Python3.13图像读写实战:从文件加载到内存操作的全流程详解(附源码)
开发语言·图像处理·python·opencv·yolo
四千岁2 小时前
如何精准统计 Token 消耗,使用对账工具控制成本?
前端·javascript·vue.js
2301_816651222 小时前
模板代码跨平台适配
开发语言·c++·算法
m0_743470372 小时前
C++代码静态检测
开发语言·c++·算法