文档对象模型,这里用到了面向对象的一些概念。简单地,我们可以把html中的每一个结点(标签)都可以看作一个dom对象,而每一个dom对象的展示形式、属性、方法可能都会不一样,但它们同属一类。
文档对象模型(Document Object Model,简称DOM),是W3C组织推荐的处理可扩展标志语言的标准编程接口。在网页上,组织页面(或文档)的对象被组织在一个树形结构中,用来表示文档中对象的标准模型就称为DOM。Document Object Model的历史可以追溯至1990年代后期微软与Netscape的“浏览器大战”,双方为了在JavaScript与JScript一决生死,于是大规模的赋予浏览器强大的功能。微软在网页技术上加入了不少专属事物,既有VBScript、ActiveX、以及微软自家的DHTML格式等,使不少网页使用非微软平台及浏览器无法正常显示。DOM即是当时蕴酿出来的杰作。
在html文档中,dom对象是按树形结构组织的。根结点是一个叫作document
的全局对象。在它下面有head
和body
等结点。
概念:
以下为例:
<html>
<head>
<title></title>
</head>
<body>
</body>
</html>
每一个html页面都是一棵Dom树,它的根节点
是html
,里面通常至少有两个孩子节点
(通常我们也称之为子节点
):head
和body
。html
是head
和body
的父亲节点
,head
和body
互为兄弟节点
。而head
和body
又会是其它节点的父亲节点
,如head
为title
的节点
,而html
则为title
的祖先节点
。
Attribute和Property的区别
特别需要提醒注意的是:html中某个节点的属性(attribute)和对应dom对象的属性(property)是不一样的,尽管有时候它们很像(像input的value和id属性),为了说明这一点,我们看下面这个简单的例子:
<script>
function test() {
const input = document.getElementById('t');
alert(input.value);
input.value = 'lalala';
alert(input.value);
alert(input.getAttribute('value'));
}
</script>
<input id="t" type="text" value="abc" />
<input type="button" value="test" onclick="test()">
上例中,id为t的一个文本框中初始显示值为它的value属性(attribute),我们在一个用户事件中,通过JavaScript脚本修改它的value属性(property)值为lalala,这时候我们在页面中看到该文本框中显示的值也已变成为了lalala,并且如果我们再获取它的value属性(property)的时候,它的值也确实改变成为了lalala。
但是,如果我们在浏览器的开发者工具中查看这个节点,我们会看到,它的value属性(attribute)依然是abc不变,通过该dom对象的getAttribute方法获取它的value属性(attribute)值也是abc。
在前一章节,我们已经介绍过Dom节点的属性,通常我们在html页面中在一个节点的开始标签的标签名称后面添加属性,不同属性之间使用空格隔开。
<tagname attr1="value1" attr2=value2 attr3='value3' attr4>
</tagname>
上例中,attr1
和attr3
的属性值都是一个字符串,但有时候我们使用双引号,在另外一些时候我们使用单引号。
多数时候,我们在书写html的时候,都应该使用双引号,这是一个习惯。但有的时候,我们也会使用单引号,典型的情况是:我们在初始化的时候将一整行数据绑定到列表的一个属性上,因为属性值必须为一个字符串,所以我们需要将数据转换成字符串,而当我们将其转换为字符串之后,通常这个字符串中便会有双引号,这时候如果属性值外层使用双引号的话,就会出错,所以这时候我们会使用单引号避免这问题。
const tpl = `<div data-value='{{=it}}'></div>`;
const data = JSON.stringify({a: 'test'});
// render(data, tpl);
事实上,attr2
的属性值也是一个字符串,所以我们定下来这样的规范,即属性值必须加引号(双引号或单引号)。这样就不会有人会误以为一些属性值为数字或布尔值。
如attr4
,有些属性甚至没有值,只是定义这个属性即可,典型的有类似disable,readonly,controls这样的属性。一般情况下,这一类属性用来决定是
和否
两种状态。
我们使用节点元素的getAttribute
来获取某一个属性值,使用setAttribute
设置属性值。
// 属性值为字符串
dom.getAttribute('attr-name');
// 属性值为数字,需要转换
parseInt(dom.getAttribute('attr-name'), 10);
// 属性值为boolean,需要使用字符串来判定
dom.getAttribute('attr-name') === 'true';
一部分DOM节点元素也提供了一些属性方便我们使用, 如id
和style
等。我们可以直接使用这些属性进行一些操作。像前面介绍过的,此属性(Property)不同于彼属性(Attribute)。
样式style
属性估计是我们使用最多的属性了,通过它,我们可以使用JavaScript脚本操作页面上的元素,使它改变颜色,形状等。如我们将某个节点元素的背景色修改为红色
node.style.backgroundColor = 'red';
如果要对页面上的一个元素进行操作,首先必须查找到该元素才可以。所以,我们首先需要掌握查找dom元素的方法。
这种方法我们在项目中几乎不会使用,这里提一下,大家做了解即可,在项目开发过程中禁止这种用法。
document.getElementById('id');
document.getElementsByClassName('classNames');
document.getElementsByName('name');
document.getElementsByTagName('tagname');
document.querySelector('tagname');
document.querySelector<HTMLInputElement>('#id');
document.querySelector<HTMLSpanElement>('.class');
document.querySelector<HTMLTextAreaElement>('[attribute-name]');
document.querySelector<HTMLAnchorElement>('[attribute-name=attribute-value]');
单个节点查找
fd.data.node.querySelector('tagname');
fd.data.node.querySelector<HTMLInputElement>('#id');
fd.data.node.querySelector<HTMLSpanElement>('.class');
fd.data.node.querySelector<HTMLTextAreaElement>('[attribute-name]');
fd.data.node.querySelector<HTMLAnchorElement>('[attribute-name=attribute-value]');
多个节点查找
Array.from(fd.data.node.querySelectorAll('tagname'));
Array.from(fd.data.node.querySelectorAll<HTMLInputElement>('.class'));
Array.from(fd.data.node.querySelectorAll<HTMLSpanElement>('[attribute-name]'));
Array.from(fd.data.node.querySelectorAll<HTMLAnchorElement>('[attribute-name=attribute-value]'));
node.parentElement as HTMLDivElement;
node.closest('tagname');
node.closest('.class') as HTMLInputElement;
node.closest('[attribute-name]') as HTMLSpanElement;
node.closest('[attribute-name=attribute-value]') as HTMLAnchorElement;
node1.contains(node2); // true or false
const child = document.createElement('tagname');
node.appendChild(child);
或
const child = document.createElement('tagname');
node.insertAdjacentElement('beforeend', child);
或
node.insertAdjacentText('beforeend', 'text');
或
node.insertAdjacentHTML('beforeend', '<tagname></tagname>');
const child = document.createElement('tagname');
node.insertAdjacentElement('afterbegin', child);
或
node.insertAdjacentText('afterbegin', 'text');
或
node.insertAdjacentHTML('afterbegin', '<tagname></tagname>');
const new_node = document.createElement('tagname');
node.parentElement.insertBefore(new_node, node);
或
const new_node = document.createElement('tagname');
node.insertAdjacentElement('beforebegin', new_node);
或
node.insertAdjacentText('beforebegin', 'text');
或
node.insertAdjacentHTML('beforebegin', '<tagname></tagname>');
const child = document.createElement('tagname');
node.insertAdjacentElement('afterend', new_node);
或
node.insertAdjacentText('afterend', 'text');
或
node.insertAdjacentHTML('afterend', '<tagname></tagname>');
node.parentElement.replaceChild(new_node, node);
node.remove();
或
node.parentElement.removeChild(node);
const deep = true;
node.cloneNode(deep);
这是一个事件触发时序的问题,多数情况下这两种区别我们并不需要关心,但如果遇到特殊情况了,那就得能搞清楚原理了。
有以下两个节点
<div>
<input type="button" value="test" />
</div>
如果在按钮上点击,div就会先捕获到事件,而input会后于div捕获到该事件,以下代码会先输出div
,后输出input
。
const div = document.querySelector('div');
const input = document.querySelector('input');
div.addEventListener('click', (e) => {
alert('capture-div');
}, true);
input.addEventListener('click', (e) => {
alert('capture-input');
}, true);
有以下两个节点
<div>
<input type="button" value="test" />
</div>
如果在按钮上点击,事件先在input上触发响应,然后才会冒泡到div上,以下代码会先输出input
,后输出div
,所捕获不同的是,addEventListener
的第三个参数为false
。
const div = document.querySelector('div');
const input = document.querySelector('input');
div.addEventListener('click', (e) => {
alert('pop-div');
}, false);
input.addEventListener('click', (e) => {
alert('pop-input');
}, false);
有聪明的朋友马上就会问:“如果我同时绑定了捕获事件和冒泡事件呢”?
<div>
<input type="button" value="test" />
</div>
如果在按钮上点击,事件先在input上触发响应,然后才会冒泡到div上,以下代码会先输出input
,后输出div
,所捕获不同的是,addEventListener
的第三个参数为false
。
const div = document.querySelector('div');
const input = document.querySelector('input');
div.addEventListener('click', (e) => {
alert('capture-div');
}, true);
input.addEventListener('click', (e) => {
alert('capture-input');
}, true);
div.addEventListener('click', (e) => {
alert('pop-div');
}, false);
input.addEventListener('click', (e) => {
alert('pop-input');
}, false);
它们的响应顺序为
capture-div
capture-input
pop-input
pop-div
有的时候,我们希望点击事件再传播,就需要将其阻止,如下例,我们希望事件不再向上冒泡,使用e.stopPropagation();
之后,果然div上面的响应不再被触发。
<div>
<input type="button" value="test" />
</div>
const div = document.querySelector('div');
const input = document.querySelector('input');
div.addEventListener('click', (e) => {
alert('pop-div');
}, false);
input.addEventListener('click', (e) => {
alert('pop-input');
e.stopPropagation();
}, false);
如果是捕获事件呢?在前文中我没有说阻止事件冒泡,而是说阻止事件传播,是因为方法stopPropagation
不仅仅是用来阻止冒泡而已,看下例:
<div>
<input type="button" value="test" />
</div>
const div = document.querySelector('div');
const input = document.querySelector('input');
div.addEventListener('click', (e) => {
alert('pop-div');
e.stopPropagation();
}, true);
input.addEventListener('click', (e) => {
alert('pop-input');
}, true);
同样的,事件只在div节点上被触发了一次。
那么,是不是stopPropagation
能够解决我们所有的问题了呢?答案是否定的,看下例
<div>
<input type="button" value="test" />
</div>
const div = document.querySelector('div');
const input = document.querySelector('input');
div.addEventListener('click', (e) => {
alert('capture-div');
}, true);
input.addEventListener('click', (e) => {
alert('capture-input');
e.stopPropagation();
}, true);
input.addEventListener('click', (e) => {
alert('pop-input');
}, false);
input.addEventListener('click', (e) => {
alert('lalala');
}, false);
div.addEventListener('click', (e) => {
alert('pop-div');
}, false);
糟糕,在capture-input
之后,pop-input
,甚至lalala
都出来了,这也许不是我们想要的结果,怎么做呢,答案是将stopPropagation
换成stopImmediatePropagation
就可以了。那么方法stopPropagation
和stopImmediatePropagation
的差别是什么呢?从名字上看,就差了一个Immediate
,英国普通话讲,这个意思是立即,马上
的意思,加上Immediate
就马上停止了。实际上,stopImmediatePropagation
确实是让事件马上
停止了传播,而stopPropagation
则是到当前响应事件的dom节点为止,这其中的差异,不可不察。
什么又是事件委托呢,事件委托是指利用事件模型,在外层的某个节点上绑定事件,而它内部的其它节点则委托这个节点
进行事件的响应处理。
例如
<ul>
<li data-value="1">test</li>
<li data-value="2">test</li>
<li data-value="3">test</li>
<li data-value="4">test</li>
<li data-value="5">test</li>
<li data-value="6">test</li>
<li data-value="7">test</li>
<li data-value="8">test</li>
<li data-value="9">test</li>
<li data-value="10">test</li>
</ul>
利用事件模型,我们只需要给外层的ul节点绑定一次事件即可,无须给每个li绑定事件
const ul = document.querySelector('ul');
ul.addEventListener('click', (e) => {
alert(e.target.dataset.value);
});
它的最佳使用场景是什么呢?就是现今移动端的列表页面。因为列表中的某一行的事件,如果交由每一行去做,监听的事件太多了,会拖慢整个页面的响应速度,某些低端的android机性能可能会是很差的。但这还不是最要命的,要命的是,列表里的内容是不固定的,通常我们是会在列表快要到底部时再加载更多内容,甚至在某些对性能要求非常严格的场景下,我们需要移除最上面已经加载过了一些内容,防止一直加载更多导致列表过大性能下降。这个时候如果不使用委托,可以想像整个页面的性能和开发的复杂程度。使用委托,只需要给列表外层的某个节点绑定一次事件就搞定了,无论是从性能还是从开发的复杂程度上讲,都是最优的。
前面我们已经提到过了一些dom节点元素的方法,以及设置和获取属性(Attribute)的方法。除了那些之外,一些特定的节点还有一些独特的方法,比如Form
有submit
方法,比如Video
有play
,stop
方法等。通过这些方法,才将一个静态的页面变成为了可以接受用户事件的页面。