正确理解 DOM
简介🧰
如果你是一名前端开发者,你可能听说过 DOM,或者在 JavaScript 中使用过一些 DOM 方法。然而,你可能并不完全了解它是什么,或者它是如何工作的。
本文将帮助您深入了解DOM 以及它如何与网页在屏幕上的渲染相协调。在此过程中,我们将介绍一些与 JavaScript 对象、浏览器和渲染相关的关键概念。这将有助于您提升 Web 开发专业知识,并让您能够更高效地使用 DOM 提供的工具,即使您正在使用 JavaScript 库或框架。
先决条件
- 熟悉 HTML、CSS 和 JavaScript
浏览器💻
首先,我们需要更好地了解设备上的网络浏览器。在本文中,我将介绍浏览器的三个核心组件。
第一个是渲染引擎(也称为浏览器引擎),它读取 HTML 和 CSS 文件,并将内容渲染(输出)到屏幕上。这是创建 DOM 的组件!它实际上可以在浏览器之外使用,例如,电子邮件客户端使用渲染引擎来显示 HTML 电子邮件。您可能听说过流行浏览器中使用的渲染引擎——Blink(Chromium 浏览器,例如 Chrome、最新版本的 Microsoft Edge 等等)、Gecko(Firefox)和Webkit(Safari)。
第二个组件是JavaScript 引擎,它读取并运行传递给它的任何 JavaScript 文件。同样,这是一个独立的组件,可以在浏览器之外运行。最受欢迎的是谷歌的V8 引擎,它被 Chromium 浏览器和 NodeJS/Deno 使用。Firefox 使用SpiderMonkey,而 Safari 的引擎名为JavaScriptCore。
第三个是JavaScript 运行时环境。这是一些允许 JavaScript 引擎访问与其运行环境相关的功能的代码。因此,在 Web 浏览器中,它提供特定于浏览器的功能,例如与 DOM 交互。相比之下,NodeJS 为 JavaScript 引擎提供了一个不同的运行时环境,该环境特定于非浏览器环境,例如服务器或命令行。
这些组件在浏览器中协同工作以生成网页。它们通常主要用 C++ 编程语言编写。
浏览器提供的核心功能与 Web 本身一样,并非集中式,而是基于某些标准。在提及浏览器向开发者提供的功能时,我会参考 Mozilla 开发者网络的 Web 文档,而不是实际的标准,因为它们更易于理解,能够帮助我们了解可用的工具以及它们在不同浏览器中的实现方式。
全局对象🌍
另一件需要正确理解的事情是JavaScript 中的对象。在编程中,我们用对象来描述世界——对象是一种小型的数据容器,用于连接其他数据。
假设我们想要描述整个世界。这个对象会包含很多属性,也就是属性。这些属性包括自然界中存在的事物,比如树木;人类发明的事物,比如手机;以及你可以做的事情,比如“吃蛋糕”。最后一个属性在 JavaScript 中是一个函数,在这种情况下,属性就被称为方法。
在我们的例子中,世界对象是“我们放置所有东西的地方”。JavaScript 也有一个类似的地方,它被称为全局对象。假设我的 JavaScript 在浏览器中运行,全局对象包含与浏览器和网页相关的属性和方法。
定义全局浏览器对象实际上代表什么非常困难。您的网页在某个标签页中运行,并包含独特的元素和事件。另一个标签页中的页面是独立的,使用其自己的全局对象运行不同的 JavaScript。因此,我们可以将全局对象称为“标签页”对象。但是,您也可以访问浏览器属性,例如浏览器历史记录和存储。那么,我们应该如何称呼它呢?
嗯,浏览器在一个名为 的变量中提供了它window
。但它并不完全代表一个用户界面窗口。它只是一个“我们放置所有内容的地方”的标签。JavaScript 可以轻松访问这个地方——我们无需指定window
访问其中的内容,只需说出来someProperty
就行了window.someProperty
(大多数情况下)。
浏览器应该在窗口对象上提供哪些功能的定义已经标准化,即使用接口。这是一个面向对象编程术语,指的是对象的描述,而不是对象本身。虽然接口通常指的是交互点,但在这里它指的是对象的描述,因为这使得对象之间的交互能够顺畅进行,因为它们知道另一个对象具有哪些属性和方法。
关于接口,我们应该了解以下两点:
-
接口名称按照惯例以 PascalCase 格式书写。
-
接口可以通过从祖先接口继承来获取其他接口的属性和方法,或者通过名为mixin的不相关接口获取它们。我们稍后会看到这一点。
Web API 💬
这是 MDN 关于窗口对象接口的文档:Window。
看一下,你会发现上面有很多内容。浏览器提供的与之通信的功能被称为Web API。
API 代表应用程序编程接口。换句话说,有人编写了一个应用程序(在本例中是浏览器),并且还编写了一组功能和规则,以便您可以通过编程与之交互。
例如,假设你在 JavaScript 代码中使用fetch()从互联网获取资源。这不属于 JavaScript 语言——你无法在非浏览器运行的 JavaScript 中使用它。但在浏览器中你可以使用它,因为浏览器在创建 window 对象时会将 fetch 方法附加到该对象上。
当您调用fetch()
或任何其他 Web API 方法时,您正在利用浏览器提供的运行时环境。这些方法的主要区别在于它们是异步的,这意味着它们不一定在 JS 代码中的上一个命令之后立即运行——您需要发出一个操作请求,该操作会排队并在可能时运行。例如,在 的情况下fetch()
,获取请求的资源时会有延迟。
Web API 使用具有属性和方法的对象,就像 window 对象一样。在 fetch API 中,Response对象就是其中之一。API 明确定义了该对象的结构。
但我们不会讨论浏览器中所有那些稀奇古怪的 API:我们想知道 DOM 是什么。首先还有一件事需要了解:window 对象中一个名为document的属性。
文档和树🌲
就像 window 对象是浏览器中几乎所有“全局”内容(控制台、滚动条、窗口尺寸等)的容器一样,document是内容(即网页本身)的容器。它代表您提供给浏览器的内容,而不是已经存在的内容。document 可以是 HTML、XML 或 SVG 文档,但我们只讨论 HTML。
你可以通过请求浏览器打开设备本地存储的 HTML 文件来获取 HTML 文件,或者你也可以请求访问某个网站,让浏览器通过互联网从该网站的服务器获取文件。浏览器的渲染引擎(本文开头提到)会执行两件事:解析HTML(逐行读取代码),然后创建元素树。
我所说的“创建树”并非指种植。它是一种用编程语言存储数据的方式,即创建彼此之间存在“家族”关系的对象。这些“家族”关系与你在 HTML 文档中创建的相同。
这些关系由边(显然应该称为“分支”,不过没关系……)定义。边末端的对象称为节点,因为它表示线条连接的地方(它也是植物上叶子和茎连接的地方,所以更接近于树的比喻)。但请记住,节点仍然只是一种对象。
树的最顶端节点称为根。从视觉上看,它的结构有点像一棵树。浏览器创建的是文档树:一个以文档为根节点的节点树。它将文档的相关信息存储在根节点中,页面上的每个 HTML 元素及其内部的任何文本也都有自己的节点。
进入 DOM📄
最后我们来谈谈 DOM。
从技术角度来说,DOM并非文档树,即数据结构本身。它是描述数据如何存储和交互的模型。然而,你经常会听到人们说“操作 DOM”,这比“操作文档树”更简单。为了方便起见,我也用这个意思来表达 DOM。
它的专业术语是“对象模型”,这意味着它定义了一些对象以及如何操作它们,但我们不必担心这一点。只需知道 DOM 的全称:文档对象模型 (Document Object Model)。
关键在于,DOM 是浏览器的 Web API 之一。我们可以获取(读取)DOM 节点的信息,并使用 JavaScript 对其进行更改(写入)。我们知道如何做到这一点,因为 DOM API 的接口中有相关描述。
需要明确的是,DOM 是用于操作文档的通用 API。HTML 有一个特定的分支,称为HTML DOM API(记住,其他类型的文档也可以用 DOM 建模)。但这种区别实际上对我们没有什么影响。
我们可以在 MDN 的DOM和HTML DOM文档中看到我们需要的接口。(目前官方的描述是 WHATWG 的DOM Living Standard,而 HTML DOM 的定义则在 WHATWG 的HTML Living Standard中。)
使用 DOM
我们通过一个例子来理解接口。
在我的 JavaScript 中(浏览器的渲染引擎通过<script>
标签在我的 HTML 文档中发现它,并且浏览器的 JavaScript 引擎以全局对象的形式运行window
),我可以访问该document
对象,正如所讨论的那样。
它由Document接口描述。在方法列表中,你会看到Document.querySelector()。这允许我使用 CSS 选择器语法从文档中获取元素——在本例中,是一个 HTML 元素,因为我们的文档就是 HTML 格式的。
现在假设<input>
我的 HTML 文件中有一个带有 id 的元素my-input
。我在 JavaScript 中写入以下内容:
const input = document.querySelector('#my-input');
当 JavaScript 引擎解析我的代码时,它需要计算出输入变量的值。该querySelector()
调用会触发运行时环境在文档树(由渲染引擎提供)中查找正确的元素(C++ 对象),将其转换为 JavaScript 对象,然后将其传递给 JavaScript 引擎。如果找不到,它会返回null
,这是一个 JavaScript 中的原始值,本质上意味着“无值”。
在我的示例中,我现在有一个指向元素对象的变量。具体来说,它是一个 HTML 输入元素,由HTMLInputElement接口(HTML DOM 的一部分)描述。从列出的属性中可以看到,我可以访问输入中的值(文本)并对其进行读写。这非常实用。
现在看看这些方法,你会看到像blur()和focus()这样的东西。它们也非常有用。但看看它们的来源——它们继承自HTMLElement。 Myinput
是 HTMLElement 的一种类型,因此它拥有所有 HTML 元素共享的属性和方法。
继承不止于此——HTMLElement 是一种Element类型(现在我们回到了通用的 DOM API)。它还有一些实用的功能,比如setAttribute(),这样我就可以在某些情况下为输入字段添加一个类。
让我们继续往上看。元素是一种Node类型。我们知道它们是什么。元素并不是唯一的节点类型——当然,文档也是一种 Node 类型,因为它是树的根节点。我们之前提到过,元素内的文本有自己的节点Text ,你可以使用textContent属性从该节点读取/写入它。
注意:这里我们可能会感到困惑,因为还有一个HTMLElement.innerText和一个Element.innerHTML属性。正如 MDN所解释的,这些属性性能较差,并且innerHTML
容易受到跨站脚本攻击(例如,我从输入框中获取值,然后在其他地方将innerHTML
a 的值设置div
为任意值——有人可能编写了一个<script>
包含恶意 JavaScript 代码的标签,并在我的页面上运行)。所以,如果我只想向元素添加文本,textContent
哪个属性更合适呢?
现在我们来到了继承链的顶端——所有这些都是EventTarget类型。Window 也是。这允许我添加或删除事件监听器,从而允许我用 JavaScript 函数响应页面事件(例如点击)。
最后要讨论的是:假设我们使用Document.querySelectorAll()获取特定类型的所有输入。注意,它返回的是一个NodeList。这很烦人,为什么不是一个 JavaScript 数组呢?好吧,记住 DOM 不是 JavaScript 的一部分——它与语言无关。例如,你可以在 Python 中使用 DOM 方法。这意味着在 JavaScript 中使用 DOM 对象与使用任何其他类型的对象并不完全相同。
DevTools 中的 DOM
方便的是,浏览器为我们提供了一些很好的工具来帮助我们查看和与 DOM 交互。
在这里,我打开了 Google 主页上的 Chrome 开发者工具并检查了他们的节日徽标img
元素:
“元素”选项卡显示了图像标签及其在文档中的位置。它看起来像是一个 HTML 标签,但其实并非如此。我们可以右键单击页面并选择“查看页面源代码”来查看原始 HTML。
其实Elements标签就是DOM的可视化表现,其中的元素就是对象。
让我们通过进入“控制台”选项卡来证明这一点。如果我们输入$0
(控制台的快捷方式,用于记录“元素”选项卡中当前选定的元素),它只会显示相同的表示。但如果我使用,console.dir
我可以看到对象:
在这里我们可以看到对象的所有属性,包括那些继承的属性。
在 JavaScript 中,一个对象继承自的对象被称为它的原型,也就是你用来构建其他对象的基类。我们的图像元素从它的原型“HTMLImageElement”继承属性和方法,而“HTMLImageElement”又从它的原型“HTMLElement”继承,以此类推。这就是一个原型链。
我们可以通过展开属性来查看原型对象__proto__
。如果我们继续沿着链向上追溯,最终会到达Object
,它包含所有JavaScript 对象继承的属性和方法。这只是为了演示——你不需要这样做。
链中的所有这些对象(除了实际的图像元素)都已存在于 JavaScript 引擎的 window 对象中。如果您console.log(window)
在空白 HTML 页面上这样做,仍然可以找到它们。当我使用 DOM 访问 logoimg
元素并将其转换为 JavaScript 对象时,其原型链已由这些对象设置。
这些属性值要么是 HTML 图像标签中的属性 (attribute),要么是使用 JavaScript 中的 DOM API 设置的,仅供浏览器识别(例如与尺寸相关的属性),要么是自对象创建以来就保留的默认值。如果您只是创建一个简单的图像元素而没有任何其他信息,则所有值都是默认值。
希望您现在对 DOM 对象是什么以及如何检查它们有了更好的了解。如果您想了解更多关于使用 Chrome DevTools 检查 DOM 的信息,Google 提供了相关指南。
渲染🎨
现在我们了解了 DOM 以及如何使用它,让我们更仔细地看看渲染页面的过程,以便我们可以更仔细地思考如何使用 DOM。
您访问的任何网站本质上都是一个 HTML 文件(简称“文档”),其中包含对其他文件(HTML、CSS 或 JavaScript)的引用,这些文件都存储在服务器上,并通过互联网发送到浏览器。浏览器解析 HTML 并开始构建 DOM。
然而,JavaScript 会影响解析过程。如果浏览器访问<script>
HTML 中的某个标签,它会默认暂停 DOM 构建,同时<script>
执行该标签中的 JavaScript 代码,因为 JavaScript 可能会使用 DOM API 修改 HTML 内容。
这就是为什么我们经常建议将<script>
标签放在 HTML 的底部,以便 HTML 能够优先加载。或者,你也可以使用script 标签上的 或 属性来defer
更改默认行为。async
浏览器还会创建CSS 对象模型(CSSOM)。它类似于 DOM,但它不代表 HTML 文档,而是通过接口来表示 CSS 样式表及其内容。
它是一个 API,因此您可以与它交互来改变您的样式,但通常最好先在样式表中定义您需要的所有样式,然后在必要时通过改变元素上的类名(或者style
如果您愿意,使用元素上的属性)来更改它们使用 DOM 所适用的内容。
为了准备渲染,DOM 和 CSSOM 会被组合起来,创建另一棵树,即渲染树。任何不会显示在页面上的内容(例如<head>
元素)都会被排除在外。渲染树包含浏览器显示网页所需的所有信息。
浏览器组装页面上元素的布局(就像在绘画之前用铅笔画草图一样),然后将元素绘制到屏幕上。
这意味着,如果我们通过更改 DOM 来响应页面上的用户交互,浏览器将不得不做一些工作来重新布局和重绘页面上的项目。这会降低性能,甚至可以说是性能开销。然而,浏览器会尽可能高效地响应事件,只进行必要的重新布局和重绘。Tali Garsiel 在其关于浏览器工作原理的研究中对此进行了解释。
请记住这一点,因为有时人们会误以为我们之所以拥有精美的前端框架是因为DOM 本身很慢。这完全说不通——框架仍然需要使用 DOM,所以它们不可能让它变得更快。实际上,这完全取决于你如何使用DOM。
让我们简单回顾一下 DOM 操作的历史和现状,以理解这一点。
库、框架和纯 JS
您经常会听到 JavaScript库和框架。库为您提供其他开发者编写的附加方法,您可以随时调用这些方法。框架对您的应用程序架构拥有更强的控制力,因此它会在适当的时候调用您代码中的函数,而不是反过来。
长期以来,jQuery 一直是编写 JavaScript 的标准方式。它是一个创建于 2006 年的库,旨在简化 DOM 操作,当时 DOM API 功能有限,且浏览器实现不一致。它至今仍在使用,有些人喜欢其简洁的语法,但现在它的核心功能可以在现代浏览器中使用纯 JavaScript 实现。
现代的库和框架并不需要解决 DOM 的缺陷,但它们确实致力于提高你使用 DOM 的效率和生产力。这并非它们存在的唯一原因,但却是一个重要的原因。
如果您正在编写一个用户交互有限的简单网站,那么只要您在 DOM 操作方面没有做出一些性能方面非常愚蠢的举动,您可能就不会遇到效率问题。但如今网络上并非只有简单的网站——像 Facebook 这样的 Web应用也非常常见。
这些应用程序包含动态且不断变化的内容,严重依赖用户输入和从服务器提取新数据。JavaScript 负责处理这些变化,是应用程序运行的核心。这与最初设计用于向浏览器提供网页的整个基础设施的初衷大相径庭。但问题不在于需要进行大量更改,而在于如何准确地告知浏览器哪些部分需要更改,从而避免不必要的重新渲染,并且避免导致任何错误。
目前最常用的核心前端库和框架是 React、Angular 和 Vue.js。它们旨在减轻你的 DOM 操作负担,让你更专注于页面的呈现效果,而不是如何实现。如果你想专业地开发 Web 应用,最好的办法就是选择其中一个框架并学习它(你不必学习,但大多数公司都会使用其中一个或类似的框架)。
如果您正在制作更简单的网站,或者只是想学习 DOM API,那么有很多关于纯 JavaScript DOM 操作的指南,比如MDN 的这个。
结论
让我们回顾一下要点:
- DOM 是浏览器提供的 API,但该术语也经常用于指代文档树。文档树是由浏览器渲染引擎创建的 HTML 文档模型。
- 浏览器窗口是浏览器 JavaScript 引擎中的全局对象。它使您可以访问 JavaScript 运行时环境的功能,包括 DOM API 的 JS 实现。DOM API 允许您与通过接口描述的文档树对象进行交互。
- 前端库和框架可以帮助您提高使用 DOM 的生产力,但您应该清楚使用它们的原因,以确保获得最佳效果。
感谢阅读,祝您 DOM 操作愉快!🙂
来源
我尽可能地交叉引用了我的资料来源。如果您认为本文中的某些信息有误,请留下您的评论或提供相关证据。🙂
* = 特别推荐进一步研究
- 浏览器引擎 - 维基百科
- JavaScript 引擎 - 维基百科
- 全局对象 - javascript.info
- 窗口 - MDN
- API - MDN 词汇表
- JavaScript 内部:JavaScript 引擎、运行时环境和 setTimeout Web API - 点点滴滴(中等)
- 树(数据结构)-维基百科
- 什么是文档对象模型? - w3.org
- *文档对象模型(及相关页面) - MDN
- * Ryan Seddon:那么浏览器究竟是如何呈现网站的?| JSConf EU 2015
- 浏览器的工作原理:现代网络浏览器的幕后 - Tali Garsiel,发表于 html5rocks.com
文档树图像来源:Birger Eriksson,CC BY-SA 3.0,通过 Wikimedia Commons(已移除侧边横幅)
本文于 2021 年 4 月 24 日更新,主要提及 JavaScript 运行时环境。
文章来源:https://dev.to/joshcarvel/properly-understanding-the-dom-2cg0