HTML 是一种相当简单的、由不同元素组成的标记语言,用来告知浏览器如何组织页面的。

文档对象模型(DOM)是 HTML 和 XML 文档的编程接口。DOM 将文档解析为一个由 DOM 节点和相关对象(包含属性和方法的对象)组成的结构集合。尽管我们通常会使用 JavaScript 来访问 DOM,但它并不是 JavaScript 的一部分,它也可以被其他语言使用。

# 1. HTML 元素

几乎所有网页都是有这样的结构组成:

<html lang="zh-cmn-Hans" style="font-size: 109.4px;">
  <head></head>
  <body></body>
</html>

其中:

(1) <html></html>元素是页面的根元素,它描述完整的网页。
(2) lang="zh-cmn-Hans" style="font-size: 109.4px;"是用来描述<html>元素的属性,属性常用来描述元素的额外信息。
(3) <head></head>元素包含了我们想包含在 HTML 页面中、但不希望显示在网页里的内容,包括 CSS 样式、Javascript 脚本、元数据描述等。
(4) <body></body>元素包含了我们访问页面时、所有显示在页面上的内容,也就是我们能看到的内容。

HTML 中的元素特别多,除了以上提到的,还包括<text><div><a>等以及各种自定义元素。

# 2. DOM 解析

我们常见的 HTML 元素,在浏览器中会被解析成节点。比如下面这样的 HTML 内容:

<html>
  <head>
    <title>文档标题</title>
  </head>
  <body>
    <a href="xx.com/xx">我的链接</a>
    <h1>我的标题</h1>
  </body>
</html>

在浏览器中会被解析成节点,如图:

HTML元素解析成节点

在控制台,我们也能比较清晰地看到这样的层级关系,如图:

控制台HTML层级关系

节点树中的节点彼此拥有层级关系,父(parent)、子(child)和同胞(sibling)等术语用于描述这些关系。父节点拥有子节点,同级的子节点被称为同胞(兄弟或姐妹)。在节点树中,顶端节点被称为根(root)。节点树相关内容还包括:

  • 除了根节点(根节点没有父节点)以外,每个节点都有父节点
  • 一个节点可拥有任意数量的子节点
  • 相同父节点的子节点,互为同胞节点

通过 HTML DOM 相关接口,我们可以使用 JavaScript 来访问节点树中的所有节点,也可以创建或删除节点。因此,所有 HTML 元素(节点)均可被修改。DOM 接口主要用于操作 DOM 节点,如常见的增删查改:

  • document.getElementById(id):根据 id 获取元素
  • document.createElement(name):创建元素
  • parentNode.appendChild(node):添加子元素
  • element.innerHTML:设置/获取元素内容

通常什么时候会用呢,最常见的便是列表的维护,包括增加新的选项、删除某个、修改某个等等。在浏览器兼容性问题很多的时候,我们常常会使用jQuery来进行些 DOM 操作,如今兼容性问题逐渐变少,大家更倾向于用原生 DOM 接口来进行操作。

随着应用程序越来越复杂,DOM 操作越来越频繁,需要监听事件和在事件回调用更新页面的 DOM 操作也越来越多,性能消耗则会比较大。于是虚拟 DOM 的想法便被人提出,并在许多框架中都有实现。虚拟 DOM 其实是用来模拟真实 DOM 的中间产物,主要包括以下功能:

  • 用 JS 对象模拟 DOM 树,简化 DOM 对象
    • 简单来说,就是用一个对象模拟 DOM 元素,保留主要的一些 DOM 属性,其他的则去掉
    • 通过这种方式,可以减少 DOM Diff 时候的计算量
  • 使用虚拟 DOM,结合操作 DOM 的接口,来生成真实 DOM
    • 使用假 DOM 生成真 DOM,同时保持真实 DOM 对象的引用,一边下一个步骤的执行
  • 更新 DOM 时,比较两棵虚拟 DOM 树的差异,局部更新真实 DOM

# 2. DOM 事件流

DOM 事件流描述的就是各个元素从页面中接受事件的顺序。DOM 事件流(event flow)存在三个阶段:

  • 事件捕获阶段(从文档的根节点流向目标对象)
  • 处于目标阶段(在目标对向上被触发)
  • 事件冒泡阶段(再回溯到文档的根节点)。

关于 DOM 事件流以及以上的三个阶段,我们需要知道:

  • 事件捕获:
    • 当鼠标点击或者触发 DOM 事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件
    • 在事件捕获的概念下在p元素上发生click事件的顺序应该是document -> html -> body -> div -> p
  • 事件冒泡:
    • 与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点
    • 在事件冒泡的概念下在p元素上发生click事件的顺序应该是p -> div -> body -> html -> document

DOM 标准事件流的触发的先后顺序为:先捕获再冒泡。也就是说,当触发 DOM 事件时会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。不同的浏览器对此有着不同的实现,IE10 及以下不支持捕获型事件,所以就少了一个事件捕获阶段,IE11、Chrome 、Firefox、Safari 等浏览器则同时存在。

# 事件委托

基于事件冒泡机制,我们可以实现将子元素的事件委托给父级元素来进行处理。当我们需要对很多元素添加事件的时候,可以通过将事件添加到它们的父节点而将事件委托给父节点来触发处理函数。这样能解决什么问题呢:

  • 绑定子元素会绑定很多次的绑定,而绑定父元素只需要一次绑定
  • 将事件委托给父节点,这样我们对子元素的增加和删除、移动等,都不需要重新进行事件绑定

常见的使用方式还是以列表为例子,每个选项都可以进行编辑、删除、添加标签等功能,而把事件委托给父元素,不管我们新增、删除、更新选项,都不需手动去绑定和移除事件。比如:

// 给id为my-list的元素绑定click事件
document.getElementById("my-list").addEventListener("click", function (e) {
  // 兼容性处理
  var event = e || window.event;
  var target = event.target || event.srcElement;
  // 判断是否匹配目标元素
  if (target.nodeName.toLocaleLowerCase === "button") {
    console.log("you clicked a button", target.innerHTML);
  }
});

这个事件委托的过程实现方式为:

(1) 在父层(或更外层)元素上绑定某类事件。
(2) 事件触发的时候,检查源元素event.target是否符合预期。
(3) 如果事件发生在我们需要处理的元素里,则进行后续的处理。

如果在列表数量内容较大的时候,对成千上万节点进行事件监听,也是不小的性能消耗。使用事件委托的方式,我们可以大量减少浏览器对元素的监听,也是在前端性能优化中比较简单和基础的一个做法。

需要注意的是,如果我们直接在document.body上进行事件委托,可能会带来额外的问题。由于浏览器在进行页面渲染的时候会有合成的步骤,合成的过程会先将页面分成不同的合成层,而用户与浏览器进行交互的时候需要接收事件。此时,浏览器会将页面上具有事件处理程序的区域进行标记,被标记的区域会与主线程进行通信。

如果我们document.body上被绑定了事件,这时候整个页面都会被标记。即使我们的页面不关心某些部分的用户交互,合成器线程也必须与主线程进行通信,并在每次事件发生时进行等待。这种情况,我们可以使用passive: true选项来解决。

# 3. BOM

BOM 即 Browser Object Model,浏览器对象模型。BOM 主要处理浏览器窗口和框架,通常浏览器特定的 JavaScript 扩展也都被看做 BOM 的一部分。BOM 是各个浏览器厂商根据 DOM 在各自浏览器上的实现,表现为不同浏览器定义有差别、实现方式不同。Javacsript 是通过访问 BOM 对象来访问、控制、修改客户端(浏览器)。

DOM(Document Object Model 文档对象模型)是为了操作文档出现的 API,包括document。BOM(Browser Object Model 浏览器对象模型)是为了操作浏览器出现的 API,包括window/location/history等。