DOM是Document Object Model的缩写,翻译过来叫做文档对象模型,它是由W3C定义的标准。它用于Web页面建模,浏览器中所展示的Web网页,都是以DOM的形式组织起来的。
DOM简介
下面是一个网页的片段:
<!doctype html>
<html>
<head>
<title>Hello DOM!</title>
</head>
<body>
DOM is Document Object Model
</body>
</html>
Live DOM Viewer可以将上面的网页以DOM的形式展示出来:
DOCTYPE: html
HTML
HEAD
#text:
TITLE
#text: Hello DOM!
#text:
#text:
BODY
#text: DOM is Document Object Model
如上所示,DOM的形态是一个节点树。它有若干种节点类型,比如:
- Document, 这是所有DOM节点数的根结点,上面的例子中没有显示
- Document Type,就是上面例子中的
DOCTYPE: html
- Element,这是HTML标签,比如
<html>
,<head>
等等 - Text,文本节点,比如上面例子中的:
Hello DOM!
,DOM is Document Object Model
- Comment, 例如:
<!-- I'm a comment -->
- Document Fragment,这个节点类型和Shadow DOM有关
- ProcessingInstruction, 这个不太常用,就不介绍了
在DOM节点树中,每个节点都有一个父节点(根结点document除外,它父节点为空);每个节点可以有若干个子节点,而且这些子节点互为兄弟节点,并且是有先后次序的。以此为基础,DOM节点树可以看成是由不同的节点按照父子和兄弟的顺序组织起来的。只要根据父大于子、兄大于弟这个规则,就可以判断出DOM节点树中的任意两个节点的先后次序。
DOM事件
Web网页是可以与用户交互的。从底层看,交互的过程是这样的,首先用户操作的时候浏览器会生成一个DOM事件,然后预置的事件侦听器响应这个DOM事件,最终对DOM的节点树进行某些修改,籍此将交互的结果反馈给用户。
通过DOM的事件机制,一个网页可以来响应外界的交互请求,这些交互请求可能包括:用户操作、网络操作以及一些脚本生成的事件。事件是异步发生的,因为DOM不知道交互请求什么时候会到来,所以网页要随时准备好应对可能发生的事件。
一个典型的例子,用户点击了网页上的提交按键,从DOM的角度,这个故事应该这么说,网页上的一个提交按键被用户点击了,于是乎就产生一个“提交”事件。如果用户是在线提交一个表格的话,那么对这个“提交”事件的处理方式一般有两种:一)表格填写正确,这个“提交”事件应该把表格数据提交到服务端处理;二)表格填写有误,中止“提交”事件,并把错误信息返回给用户,然后等待用户修改完表格之后重新提交。
在DOM事件的处理过程中有几个参与者:
- 事件(Event)
- 事件标的(EventTarget)
- 事件侦听者(EventListener)
当交互发生时,一个事件被生成,等待被派发(Dispatch)。当事件被派发后,就开始再DOM节点树中传播,目标是要到达事件标的。在这个过程中,沿途的节点如果埋伏有事件侦听者,就会触发相应的处理逻辑。具体的行为,会在下面的章节说明。
事件的传播过程
首先是「事件(Event)」本身,当浏览器检测到一个用户操作时,比如用户点击了一个按键。会产生一个类型为“点击”的事件,这个事件的标的是被点击的按键这个DOM节点。同时,在事件发生时,这个事件的传播路径也被确定下来。
比如下面的例子:
<html>
<body>
<button>Hello, world!</button>
</body>
</html>
当上面的Hello, world!
按键被点击的时候,产生了一个类型为click
的事件,这个事件的标的是button
节点。但是由于标的处在节点树中,所以这个事件要从的根结点(也就是document
)往下传播,如下所示:
document
|--html
|-- body
|-- button
事件的传播路径可以从事件的composedPath
读取到。
事件带有一个属性,用来表征事件阶段(EventPhase)。事件被创建时,它处于起始阶段,EventPhase为NONE;当事件从document
节点开始往事件标的传播时,EventPhase为CAPTURING_PHASE;当事件到达事件标的的时候,EventPhase为AT_TARGET;可能会令你惊奇的是,当事件到达标的之后,会从原路径遣返,向上冒泡到根节点,这时候EventPhase为BUBBLING_PHASE。
当事件传播结束时,就会触发浏览器对该事件的预置操作。比如用户点击了一个提交按键,那么浏览器的预置操作就是把当前页面的内容提交到服务器。
操控事件传播
事件的传播在一定程度上是可以操控的。首先,当前DOM节点上埋伏的事件的侦听者可以调用事件的stopPropagation()
方法来阻止事件向后续的DOM节点传播。另外,事件还有stopImmediatePropagation()
方法,不仅有stopPropagation()
的效果,还可以阻止当前DOM节点上随后的事件侦听者对事件进行处理。
如果stopPropagation()
是为了阻止事件在DOM数传播,那么preventDefault()
则是用来阻止触发浏览器针对事件的预置行为。但是,但是,preventDefault()
依赖于事件的cancelable
标志,只有该标志为true的情况,preventDefault()
才可以生效。cancelable
是在事件创建的时候可以设置。
另外一个再事件创建时可以设置的属性标志是bubbles
。这个标志是false的时候,事件到达事件标的的时候不会向上冒泡;只有当这个事件是true的时候,事件才会冒泡。
事件标的和事件侦听者
所有的DOM节点都可以作为事件标的。或者从面向对象的角度,所有的DOM节点都是从「事件标的」这个类型派生出来的。
通过DOM节点的addEventListener()
方法,可以在该节点上埋伏事件侦听者。一个DOM节点可以埋伏多个事件侦听者。addEventListener()
一般有两个参数,第一个参数用来描述事件类型,比如click
事件类型用来表示这个事件是由用户点击产生的;第二个参数是用来指定事件侦听者的回调函数,在事件到达该节点的时候触发。
addEventListener()
还有一个可选参数,是一个标志,叫做capture
。如果这个标志为true,那么这个事件侦听者在事件的去途(也就是EventPhase为CAPTURING_PHASE的时候触发;否则在事件的EventPhase为BUBBLING_PHASE时候触发。
一个例子
<!doctype html>
<html>
<head>
<title>Boring example</title>
</head>
<body>
<p>Hello <span id=x>world</span>!</p>
<script>
function test(e) {
debug(e.target, e.currentTarget, e.eventPhase)
}
document.addEventListener("hey", test, {capture: true})
document.body.addEventListener("hey", test)
var ev = new Event("hey", {bubbles:true})
document.getElementById("x").dispatchEvent(ev)
</script>
</body>
</html>
上面这个例子来自于WHATWG DOM标准的2.1. Introduction to “DOM Events”
上面的例子定义了一个类型为“hey”的事件,这个事件的标的是document->body->p->span
。由于事件创建时指定bubbles
标志为真,所以这个事件既有去途(也就是从document到span)也有返途(也就是从span到document)。上面的例子中还定义了两个事件侦听者,一个侦听者埋伏在document节点,由于设置了{capture: true}
,所以该侦听者在事件的去途侦听事件;另外一个侦听者埋伏在body节点上,并在事件的返途真听事件。
上述的例子由浏览器加载后,会在浏览器的console上显示这两次事件侦听的经历。
参考链接
UIEVENTS对本文描述的过程有更深入的解读。
- WHATWG DOM标准
- Live DOM Viewer
- What’s the difference between event.stopPropagation and event.preventDefault?
(完)