第 5 章 DOM与事件
前言
本章是第五章DOM与事件相关的内容。
DOM是文档对象模型,全称为Document Object Model。DOM用一个逻辑树来表示一个文档,树的每个分支终点都是一个节点,每个节点都包含着对象。DOM提供了对文档结构化的表述,通过绑定不同的事件可以改变文档的结构、样式和内容,从而能实现“动态”的页面。
在学完后,希望掌握下面知识点:
- DOM选择器
- 常用的DOM操作
- 事件流和事件处理程序
- Event对象
- 事件委托
- contextmenu右键事件
- 文档加载完成事件
- 浏览器重排和重绘
5.1 DOM选择器
DOM选择器用于快速定位DOM元素。在原生的JavaScript中有提供根据id
、name
等属性来查找的传统选择器,也有新型的、更高效的querySelector
选择器和querySelectorAll
选择器,支持丰富的元素、属性、内容选择等。
5.1.1 传统原生JavaScript选择器
(1)通过id定位
getElementById()
函数:通过id定位元素,返回匹配到id的第一个元素。
document.getElementById('one').innerText;
一般都会避免写具有相同 id 元素的HTML页面,但如果发生了,则只会匹配第一个元素。
(2)通过class定位
getElementsByClassName()
函数:通过类名定位元素,返回由匹配到的元素构成的HTMLCollection
对象,它是一个类数组结构。
对于下面的HTML和JS:
<li class="one">节点1.2</li>
<li class="one">节点1.3</li>
document.getElementsByClassName('one');
返回值为一个HTMLCollection对象,里面包含匹配到的两个li元素值:
HTMLCollection(2) [li.one, li.one]
- 0: li.one
- 1: li.one
- length: 2
- __proto__: HTMLCollection
(3)通过name属性定位
getElementsByName()
函数:通过元素的name属性进行定位,返回由匹配到的元素构成的NodeList
对象,它是一个类数组结构。
对于下面的HTML和JS:
<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>
document.getElementsByName('node');
返回的值为一个NodeList对象,里面包含匹配到的name属性为“node”的元素
NodeList(3) [li, li, li]
- 0: li
- 1: li
- 2: li
- length: 3
- __proto__: NodeList
(4)通过标签名定位
getElementsByTagName()
函数:通过标签名定位元素,返回由匹配到的元素构成的HTMLCollection
对象。
通过标签名获取页面上的两个ul元素:
document.getElementsByTagName('ul');
返回值为一个HTMLCollection对象,里面包含匹配到的两个ul元素值:
HTMLCollection(2) [ul, ul]
- 0: ul
- 1: ul
- length: 2
- __proto__: HTMLCollection
5.1.2 新型的querySelector选择器和querySelectorAll选择器
querySelector选择器和querySelectorAll选择器能更高效地使用选择器,让其定位到特定的元素或者子元素中。都是按照CSS选择器的规范来实现的。
(1)querySelector选择器
返回的是在基准元素下,选择器匹配到的元素集合中的第一个元素:
element = baseElement.querySelector(selectors);
- baseElement:基准元素。返回的元素必须是匹配到的基准元素的第一个子元素。该基准元素可以为Document,也可以为基本的Element
- selectors:是一个标准的CSS选择器,而且必须是合法的选择器,否则会引起语法错误
- 返回值:匹配到的第一个子元素。匹配的过程中不仅仅针对基准元素的后代元素,实际上会遍历整个文档结构,包括基准元素和它的后代元素以外的元素。实际处理过程是首先创建一个匹配元素的初始列表,然后判断每个元素是否为基准元素的后代元素,第一个属于基准元素的后代元素将会被返回
例子1, 获取class为content的元素的第一个span元素:
document.querySelector('.content span').innerText;
例子2,获取第一个span或者h5元素:
document.querySelector('h5, span').innerText;
(2)querySelectorAll选择器
querySelectorAll选择器与querySelector选择器类似,区别在于querySelectorAll选择器会返回基准元素下匹配到的所有子元素的集合
elementList = baseElement.querySelectorAll(selectors);
它同样包含基准元素与选择器,返回值是一个NodeList的集合
5.2 HTMLCollection对象与NodeList对象
HTMLCollection对象与NodeList对象都是DOM节点的集合,但是在节点处理方式上是有差异的。
5.2.1 HTMLCollection对象
HTMLCollection对象具有length
属性,返回集合的长度,可以通过item()
函数和namedItem()
函数来访问特定的元素。
(1)item()函数
item()
:通过序号来获取特定的某个节点,超过索引则返回null
。
<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()函数定位第一个和第三个子元素,输出结果如下所示:
<p class="first">first</p>
<p class="third">third</p>
(2)namedItem()函数
namedItem()
:用来返回一个节点。首先通过id
属性去匹配,然后如果没有匹配到则使用name
属性匹配,如果还没有匹配到则返回 null。当出现重复的 id或者 name属性时,只返回匹配到的第一个值。
<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,因此返回id匹配到的项
<input type="text" id="username">
5.2.2 NodeList对象
NodeList对象也具有length
属性,返回集合的长度,也同样具有item()
函数,通过 索引定位子元素的位置。由于和HTMLCollection对象的item()函数一致,这里就不赘述了。
5.2.3 HTMLCollection对象和NodeList对象的实时性
HTMLCollection对象和NodeList对象并不是历史文档状态的静态快照,而是具有实时性的。对DOM树新增或者删除一个相关节点,都会立刻反映在HTMLCollection对象与NodeList对象中。
HTMLCollection对象与NodeList对象都只是类数组结构,并不能直接调用数组的函数。而通过call()函数和apply()函数处理为真正的数组后,它们就转变为一个真正的静态值了,不会再动态反映DOM的变化。
NodeList对象与HTMLCollection对象相比,存在一些细微的差异,主要表现在不是所有的函数获取的NodeList对象都是实时的。例如通过querySelectorAll()
函数获取的NodeList对象就不是实时的。
(1)相同点
- 都是类数组对象,有
length
属性,可以通过call()
函数或apply()
函数处理成真正的数组 - 都有
item()
函数,通过索引定位元素 - 都是实时性的,DOM树的变化会及时反映到HTMLCollection对象和NodeList对象上,只是在某些函数调用的返回结果上会存在差异
(2)不同点
- HTMLCollection对象比NodeList对象多个
namedItem()
函数,可以通过id或者name属性定位元素 - HTMLCollection对象只包含元素的集合(Element),即具有标签名的元素;而NodeList对象是节点的集合,既包括元素,也包括节点,例如text文本节点
5.3 常用的DOM操作
文档树是由各种类型节点构成的集合,DOM操作实际是对文档结构中节点的操作。文档结构树中的节点类型众多,但是操作的主要节点类型为元素节点、属性节点和文本节点。
下面通过一段完整的HTML代码来看看主要由元素节点、属性节点和文本节点构成的文档树结构。
<!DOCTYPE html>
<html>
<head>
<title>文档标题</title>
</head>
<body>
<a href="http://www.mianshiting.com">我的链接</a>
<h1>我的标题</h1>
</body>
</html>
如果将这些节点画成一个树的话:
- 元素节点:是拥有一对开闭合标签的元素整体,例如常见的div、ul、li标签都是元素节点
- 属性节点:是元素节点具有的属性,例如图中a标签的href属性
- 文本节点:是DOM中用于呈现文本内容的节点,例如图中h1标签内部的“我的标题”
其中元素节点和文本节点存在父子关系,而元素节点与属性节点并不存在父子关系。
常用的DOM操作包括查找节点、新增节点、删除节点、修改节点。其中查找节点在上面 5.1 DOMu选择器 已经介绍过,因此下面会记录另外三个操作。
5.3.1 新增节点
新增节点分为 2 步:
- 新建节点
- 将节点添加至指定的位置
比如针对下面的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>
接下来会完成下面这些操作:第一步,在ul的末尾添加一个li元素,其类名为“last”,内容为“新增文本1”;第二步,在新增的li之前再新增第二个li,内容为“新增文本2”。
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); // 将文本节点作为元素节点的子元素
container.appendChild(newLiOne); // 使用appendChild()函数将新增元素节点添加至末尾
var newLiTwo = document.createElement('li'); // 新创建第二个元素节点
var newTextTwo = document.createTextNode('新增文本2'); // 新创建第二个文本节点
newLiTwo.appendChild(newTextTwo); // 将文本节点作为元素节点的子元素
container.insertBefore(newLiTwo, newLiOne); // 使用insertBefore()函数将节点添加至第一个新增节点的前面
额外知识:在新增属性节点时,还有另外一种更简单的setAttribute()
函数。以上面代码为例, 可以通过下面这一行代码完成上述创建属性节点并赋值以及将属性节点绑定在元素节点上这三步共3行代码的功能:
newLiOne.setAttribute('class', 'last');
但是set Attribute()
函数不兼容IE8及更早的版本,使用时需要考虑兼容性。
5.3.2 删除节点
删除节点包含 3 种:
- 删除元素节点
- 删除属性节点
- 删除文本节点
比如对于下面的代码:
<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元素节点
删除一个元素节点需要 3 步:
- 获取该元素的父元素
var main = document.querySelector('#main');
- 获取待删除节点。应该使用
firstElementChild
属性,不能使用firstChild
属性(在此例中实际为一个换行符)
var firstChild = main.firstElementChild;
- 通过父节点,调用removeChild()函数删除该节点
main.removeChild(firstChild);
(2)删除a标签的href属性
删除一个元素的属性需要 2 步:
- 获取该元素
var link = document.querySelector('#link');
- 通过元素节点,调用removeAttribute()函数删除指定属性节点
link.removeAttribute('href');
(3)删除ul最后一个li元素的文本节点
删除一个元素的文本节点需要 3 步(方法一):
- 获取元素节点。同样需要注意要用
lastElementChild
属性而不是lastChild
属性
var lastChild = main.lastElementChild;
- 获取文本节点。在获取文本节点时,需要使用的是childNodes属性,然后取返回的NodeList对象的第一个值。不能使用children属性,因为children属性返回的是HTMLCollection对象,表示的是元素节点,不包括文本节点内容
var textNode = lastChild.childNodes[0];
- 通过元素节点,调用removeChild()函数删除指定的文本节点
lastChild.removeChild(textNOde);
方法二:将元素节点的innerHTML 属性设置为空。
在删除文本节点时更推荐使用这个方法。
lastChild.innerHtml = '';
5.3.3 修改节点
修改节点包含 3 种:
- 修改元素节点
- 修改属性节点
- 修改文本节点
下面的修改都会基于下面的代码:
<div id="main">
<!-- 测试修改元素节点 -->
<div id="div1">替换之前的元素</div>
<!-- 测试修改属性节点 -->
<div id="div2" class="classA" style="color: green;">这是修改属性的节点</div>
<!-- 测试修改文本节点 -->
<div id="last">这是最后一个节点内容</div>
</div>
(1)修改元素节点
一般直接将节点元素替换为另一个元素,使用replaceChild()
函数。
replaceChild()
函数的调用方是父元素,接收 2 个参数,第 1 个参数表示新元素,第 2 个参数表示将要被替换的旧元素。
<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)修改属性节点
有 2 种处理方式:
- 通过
getAttribute()
函数和setAttribute()
函数获取和设置属性节点值 - 直接修改属性名(需要注意,直接修改的属性名与元素节点中的属性名不一定是一致的。就像class这个属性,因为它是JavaScript中的关键字,是不能直接使用的,所以需要使用className来代替)
下面是修改元素节点的class属性和style属性的color值:
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属性修改为需要的文本内容即 可
var last = document.querySelector('#last');
// 直接修改innerHTML属性
last.innerHTML = '这是修改后的文本内容';
//如果设置的innerHTML属性值中包含HTML元素,则会被解析
//使用如下代码进行验证
last.innerHTML = '<p style="color: red">这是修改后的文本内容</p>';
//在浏览器中渲染后,可以看到“这是修改后的文本内容”为红色
5.4 事件流
在浏览器中,JavaScript和HTML之间的交互是通过事件去实现的。
在事件发生时,会相对应地触发绑定在元素上的事件处理程序,以处理对应的操作。
事件流描述的是从页面中接收事件的顺序。
一个完整的事件流实际包含了3个阶段:
- 事件捕获阶段:主要表现是不具体的节点先接收事件,然后逐级向下传播,最具体的节点最后接收到事件
- 事件目标阶段:表示事件刚好传播到用户产生行为的元素上,可能是事件捕获的最后一 个阶段,也可能是事件冒泡的第一个阶段
- 事件冒泡阶段:主要表现是最具体的元素先接收事件,然后逐级向上传播,不具体的节点最后接收事件
如果有元素绑定了捕获类型事件,则会优先于冒泡类型事件而先执行。
5.5 事件处理程序
事件处理程序,就是响应某个事件的函数,例如onclick()
函数、onload()
函数就是响应单击、加载事件的函数,对应的是一段JavaScript的函数代码。
根据W3C DOM标准,事件处理程序分为DOM0、DOM2、DOM3这3种级别的事件处理程序。由于在DOM1中并没有定义事件的相关内容,因此没有所谓的DOM1级事件处理程序。
5.5.1 DOM0级事件处理程序
DOM0级事件处理程序是将一个函数赋值给一个事件处理属性。
有 2 种表现形式:
- 先通过JavaScript代码获取DOM元素,再将函数赋值给对应的事件属性
var btn = document.getElementById("btn");
btn.onclick = function(){}
- 直接在HTML中设置对应属性的值(执行的函数体 或 函数名+script标签中定义该函数)
//设值方式一:执行的函数体
<button onclick="alert('面试厅');">单击</button>
//设值方式二:函数名+script标签中具体定义
<button onclick="clickFn()">单击</button>
<script>
function clickFn() {
alert('面试厅');
}
</script>
两种DOM0级事件处理程序同时存在时,第一种在JavaScript中定义的事件处理程序会覆盖掉后面在html标签中定义的事件处理程序。
DOM0级事件处理程序只支持事件冒泡阶段
优点:简单且可以跨浏览器
缺点:一个事件处理程序只能绑定一个函数
5.5.2 DOM2级事件处理程序
在DOM2级事件处理程序中,当事件发生在节点时,目标元素的事件处理函数就会被触发,而且目标元素的每个祖先节点也会按照事件流顺序触发对应的事件处理程序。
支持对同一个事件绑定多个处理函数。
规定了添加事件处理程序和删除事件处理程序的方法。
IE10及以下版本,只支持冒泡阶段,attachEvent()
函数添加事件处理程序,detachEvent(
)函数删除事件处理程序:
element.attachEvent("on"+ eventName, handler); //添加事件处理程序
element.detachEvent("on"+ eventName, handler); //删除事件处理程序
IE11及其他非IE浏览器,同时支持事件捕获和事件冒泡阶段。addEventListener()
函数添加事件处理程序,removeEventListener()
函数删除事件处理程序:
addEventListener(eventName, handler, useCapture); //添加事件处理程序
removeEventListener(eventName, handler, useCapture); //删除事件处理程序
- useCapture:表示是否支持事件捕获。true表示支持事件捕获,false表示支持事件冒泡,默认为false
在IE浏览器中,使用attachEvent()函数为同一个事件添加多个事件处理函数时, 会按照添加的相反顺序执行
在IE浏览器下,使用attachEvent()
函数添加的事件处理程序会在全局作用域中运行,因此this
指向全局作用域window
。
在非IE浏览器下,使用addEventListener()
函数添加的事件处理程序在指定的元素内部执行,因此this
指向绑定的元素
可以通过EventUtil
工具类来进行与事件有关的兼容性处理
5.5.3 DOM3级事件处理程序
在DOM2级事件基础上重新定义了事件,并添加了一些新的事件。
与DOM2最重要的区别在于允许自定义事件,自定义事件由 createEvent("CustomEvent")
函数创建,返回的对象有一个initCustomEvent()
函数,通过传递对应的参数可以自定义事件。可以通过dispatchEvent()
函数去手动触发,触发自定义事件的元素需要和绑定自定义事件的元素为同一个元素。
函数可以接收以下 4 个参数:
type
:字符串、触发的事件类型、自定义,例如“keyDown”“selectedChange”bubble
(布尔值):事件是否可以冒泡cancelable
(布尔值):事件是否可以取消detail
(对象):任意值,保存在event对象的detail属性中
下面通过一个具体的例子来看整个过程:
要实现的场景是,在页面初始化时创建一个自定义事件myEvent,页面上有 个div监听这个自定义事件myEvent,同时有一个button按钮绑定了单击事件;当我们单击button时,触发自定义事件,由div监听到,然后做对应的处理
可以分 3 步实现:
- 创建自定义事件
- 监听自定义事件
- 触发自定义事件
首先是html代码
<div id="watchDiv">监听自定义事件的div元素</div>
<button id="btn">单击触发自定义事件</button>
接下来就是JavaScript实现
var customEvent;
// 创建自定义事件
(function () {
if (document.implementation.hasFeature('CustomEvents', '3.0')) { //判断是否支持DOM3级事件处理程序
var detailData = {name: 'kingx'};
customEvent = document.createEvent('CustomEvent');
customEvent.initCustomEvent('myEvent', true, false, detailData);
}
})();
//监听自定义事件
// 获取元素
var div = document.querySelector('#watchDiv');
// 监听myEvent事件
div.addEventListener('myEvent', function (e) {
console.log('div监听到自定义事件的执行, 携带的参数为: ', e.detail);
});
//触发自定义事件
// 获取元素
var btn = document.querySelector('#btn');
// 绑定click事件,触发自定义事件
btn.addEventListener('click', function () {
div.dispatchEvent(customEvent);
});
5.6 Event对象
事件在浏览器中是以Event对象的形式存在的,每触发一个事件,就会产生一个 Event对象。该对象包含所有事件相关信息,包括事件的元素、事件的类型及其他与特定事件相关的信息。
5.6.1 获取Event对象
方式有 2 种:
- 在事件处理程序中,Event对象会作为参数传入,参数名为event
- 在事件处理程序中,通过window.event属性获取Event对象
我们在同一个事件处理程序中,可以使用上述两种方式获取event对象并输出:
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);
});
不同浏览器支持方式不同,可能并不能2种方式都支持,因此需要考虑兼容性处理。
5.6.2 获取事件的目标元素
- IE浏览器中:event对象使用
srcElement
属性来表示事件的目标元素 - 非IE浏览器中,event对象使用
target
属性来表示事件的目标元素。某些也能支持使用srcElement
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);
});
不同浏览器也不同,也需要考虑兼容性
5.6.3 target属性与currentTarget属性
target属性与currentTarget属性都可以表示事件的目标元素,但是在事件流中两者有不同意义
- target:在事件目标阶段,理解为真实操作的目标元素,在事件流的任何阶段,target属性始终指向的是实际操作的元素
- currentTarget:在事件捕获、事件目标、事件冒泡 3 个阶段,理解为当前事件流所处的某个阶段对应的目标元素
5.6.4 阻止事件冒泡
有时我们并不想要事件进行冒泡,比如有一个表示学生信息的容器ul,每个li元素都表示一个学生的基本信息,单击li元素会改变li的背景色以表示选中的标识。在每个li元素内部会有一个表示删除的button按钮,单击button按钮则会提示是否删除,单击确定则会删除该元素。
如果不阻止冒泡,那么在 li 内部点button时,事件同样会上升到父元素 li 上,如果在那里有操作则也会被触发。
解决方法就是阻止冒泡,在上面例子中的话就是button按钮的click事件中调用event.stopPropagation()
函数。
其实还有一个stopImmediatePropagation()
函数,这两个有一定不同:
stopPropagation()
:仅会阻止事件冒泡,其他事件处理程序仍然可以调用stopImmediatePropagation()
:不仅会阻止冒泡,也会阻止其他事件处理程序 的调用
5.6.5 阻止默认行为
通过event.preventDefault()
函数实现。
比如这个场景,限制用户输入的值只能是数字和大小写字母,其他的值则不能输入,如输 入其他值则给出提示信息,提示信息在两秒后消失
<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>
5.7 事件委托
事件委托是利用事件冒泡原理,管理某一类型的所有事件,利用父元素来代表子元素的某一类型事件的处理方式。
具体会根据 2 种常见场景来理解和分析,在下面两种场景中,事件委托会起关键作用:
- 已有元素的事件绑定
- 新创建元素的事件绑定
5.7.1 已有元素的事件绑定
场景一:假如页面上有一个ul标签,里面包含1000个li子标签,我们需要在单击每个li时,输出li中的文本内容。
方法一:常规做法。给每个li标签绑定一个click事件,在click 事件中输出li标签的文本内容
<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>
但这样性能会极差。
方法二:使用事件委托。
// 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);
}
});
事件是绑定在父元素ul上的,不管子元素li有多少个,也不会影响到页面中事件处理程序的个数,因此可以极大地提高浏览器的性能。
对于不同元素做不同处理也是可以的。
场景二:在页面上有4个button按钮,分别表示增加、删除、修改、查询这4个功能。每个按钮绑定相同的click事件处理程序,但是具体的行为不同。在这4个按钮触发click事件后,分别输出“新增”“删除”“修改”“查询”等文字。
方法一:在获取4个button后同时绑定click事件处理程序,在事件回调中输出对应的文字
<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进行比较,输出对应的文
字
// 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;
}
});
5.7.2 新创建元素的事件绑定
场景:在页面上有一个button按钮,单击button按钮会创建一个新的li元素,单击新创建的li元素,输出它的文本内容
方法一:手动绑定
<button id="add">新增</button>
// 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);
});
}
// 封装一个遍历添加click事件处理程序,用于新增元素后刷新元素
function bindEvent() {
for (var i = 0; i < children.length; i++) {
children[i].addEventListener('click', function () {
console.log(this.innerText);
});
}
}
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);
// 重新添加事件处理程序
bindEvent();
});
我们通过querySelectorAll()
函数获取到的li元素虽然会实时感知到数量的变化,但并不会实时增加对事件的绑定。因此需要在添加完新元素后重新进行手动绑定。
这样一是操作繁琐,二是会影响性能
方法二:事件委托
使用事件委托机制,我们可以更加方便快捷地实现新创建元素的事件绑定。由于事件委托机制是利用的事件冒泡机制,即使在元素自身没有绑定事件的情况下,事件仍然会冒泡到父元素中,因此对于新增的元素,只要处理事件流就可以触发其事件。
<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中的一样,不过不需要重新添加事件处理程序
5.8 contextmenu右键事件
Context Menu是一个与用户进行友好交互的菜单,例如鼠标的右键产生的效果。默认情况下,在网页上右击可以看到“重新加载”“打印”“查看页面源码”等选项;在图片上右击会出现“保存至本地”“另存为”等选项。
也可以通过contextmenu
事件来实现一个定制化的鼠标右键效果。
此处主要是具体的实现,不重复摘抄了,需要时再去看书中对应章节
5.9 文档加载完成事件
由于后续会使用Vue,因此jQuary相关的就不记录在此了,本小节只记录一下原生javascript涉及加载完成的知识。
原生javascript中有load事件,onload事件的触发表示页面中包含的图片、flash等所有元素都加载完成。jQuery中有ready事件,此处不过多记述。
5.9.1 load事件
load事件会在页面、脚本或者图片加载完成后触发。其中,支持onload事件的标签有body、frame、frameset、iframe、img、link、script
如果load事件用于页面初始化,则有 2 种实现方式:
(1)在body标签上使用onload属性
类似于onclick属性的设置,其实就是DOM0级事件处理程序
<!-- 使用onload属性 -->
<body onload="bodyLoad()">
<script>
function bodyLoad() {
console.log('文档加载完成,执行onload方法');
}
</script>
</body>
(2)设置window对象的onload属性
属性值为一个函数
<script>
window.onload = function () {
console.log('文档加载完成,执行onload方法');
};
</script>
(3)两种同时使用时的优先级
在load事件的两种实现方式中,第一种方式的优先级会高于第二种方式,如果同时采用两种方式,则只有第一种方式会生效。也就是body标签上使用onload属性的优先级比设置window对象的onload属性更高。
5.9.2 ready事件
ready事件不同于load事件,ready事件只需要等待文档结构加载完成就可以执行,不需要加载完全部资源。
因此在很多场景中,我们更推荐在ready事件中做初始化处理。
需要注意的是,ready事件并不是原生JavaScript所具有的,而是在jQuery中实现的,ready事件挂载在document对象上
5.10 浏览器的重排和重绘
HTML的渲染过程如下:
- HTML文件被HTML解析器解析成对应的DOM树,CSS样式文件被CSS解析器解析生成对应的样式规则集
- DOM树与CSS样式集解析完成后,附加在一起形成一个渲染树
- 节点信息的计算,即根据渲染树计算每个节点的几何信息
- 渲染绘制,即根据计算完成的节点信息绘制整个页面
重绘和重排发生在后两步,对它们的理解将有助于我们使用一定的手段进行性能优化
5.10.1 重排
因为浏览器渲染页面默认是基于流式布局的,因此对某一个DOM节点信息进行修改时,就需要对该DOM结构进行重新计算。
而重排的过程就发生在DOM节点信息修改的时候,重排实际是根据渲染树中每个渲染对象的信息,计算出各自渲染对象的几何信息,例如DOM元素的位置、尺寸、大小等,然后将其安置在界面中正确的位置。
因此一般发生大小、位置、字体变化或添加删除可见的DOM元素会引起重排
5.10.2 重绘
重绘只是改变元素在页面中的展现样式,而不会引起元素在文档流中位置的改变。例如更改了元素的字体颜色、背景色、透明度等,浏览器均会将这些新样式赋予元素并重新绘制。
5.10.3 性能优化
浏览器的重排与重绘是比较消耗性能的操作,所以我们应该尽量地减少重排与重绘的操作,这也是优化网页性能的一种方式
(1)将多次改变样式的属性操作合并为一次
比如将要修改的css属性合并为一个class类,再通过JS直接修改元素的class类
(2)将需要多次重排的元素设置为绝对定位
需要进行重排的元素都是处于正常的文档流中的,如果这个元素不处于文档流中,那么它的变化就不会影响到其他元素的变化,这样就不会引起重排的操作。常见的操作就是设置其position为absolute或者fixed。
假如一个页面有动画元素,如果它会频繁地改变位置、宽高等信息,那么最好将其设置为绝对定位。
(3)在内存中多次操作节点,完成后再添加至文档树中
(4)将要进行复杂处理的元素处理为display属性为none,处理完成后再进行显示
因为display属性为none的元素不会出现在渲染树中,所以对其进行处理并不会引起其他元素的重排。当我们需要对一个元素做复杂处理时,可以将其display属性设置为none,操作完成后,再将其显示出来,这样就只会在隐藏和显示的时候引发两次重排操作。
(5)将频繁获取会引起重排的属性缓存至变量
(6)尽量减少使用table布局
如果table中任何一个元素触发了重排的操作,那么整个table都会触发重排的操作,尤其是当一个table内容比较庞大时,更加不推荐使用table布局。
如果不得已使用了table,可以设置table-layout:auto或者是table-layout:fixed。这样可以让table一行一行地渲染,这种做法也是为了限制重排的影响范围。
(7)使用事件委托绑定事件处理程序
(8)利用DocumentFragment操作DOM节点
DocumentFragment是一个没有父级节点的最小文档对象,它可以用于存储已经排好版或者尚未确定格式的HTML片段。DocumentFragment最核心的知识点在于它不是真实DOM树的一部分,它的变化不会引起DOM树重新渲染的操作,也就不会引起浏览器重排和重绘的操作,从而带来性能上的提升。
因为DocumentFragment具有的特性,在需要频繁进行DOM新增或者删除的操作中,它将变得非常有用。一般的操作方法分为以下两步:
- 将需要变更的DOM元素放置在一个新建的DocumentFragment中,因为DocumentFragment不存在于真实的DOM树中,所以这一步操作不会带来任何性能影响
- 将DocumentFragment添加至真正的文档树中,这一步操作处理的不是DocumentFragment自身,而是DocumentFragment的全部子节点。对 DocumentFragment的操作来说,只会产生一次浏览器重排和重绘的操作,相比于频繁操作真实DOM元素的方法,会有很大的性能提升