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对本文描述的过程有更深入的解读。

(完)