原生 DOM 和 虚拟 DOM 详解

DOM 是什么?

DOM 全称 Document Object Model,文档对象模型。
DOM 是浏览器解析 HTML/XML 文档的结构化对象模型,它将网页内容表示为有层次的节点树,每个节点对应文档中的一个元素(如标签、属性、文本等)。
DOM 不是单一对象,它是由许多节点对象组成的树结构
比如如下网页代码:

<!Doctype html>
<html>
    <head>
        <title>My Title</title>
    </head>
    <body>
        <a href="https://www.xxx.com">My Link</a>
        <h1>My Header</h1>
    </body>
</html>

转换为 DOM 树为:

这是简化的 DOM 树,实际的 DOM 树非常复杂庞大,因为它继承自一系列的原型链。

HTML 元素类的总体继承关系如下:

DOM 的特点:

  • 存在于浏览器内存中
  • 采用父子节点关系的层次结构
  • 包含多种节点类型
  • 提供操作节点的方法和属性,js 可以修改 DOM 并实时反映在页面上

DOM 和 js 的关系

DOM 是由浏览器实现的,它不是 js 的一部分,DOM 提供了一种编程接口,允许 js 动态访问、修改、添加、删除网页内容、结构、样式。

DOM 解析

DOM 解析是指浏览器将 HTML/XML 文档转换为 DOM 树的过程。

具体过程为:浏览器接受 HTML 文档 -> HTML 解析器将 HTML 转换为 tokens -> tokens 转换为 nodes -> nodes 被组织成 DOM 树

HTML 解析器是如何工作的?

HTML 解析器是浏览器的一部分,它负责将 HTML 字节流转换为 DOM 树。

网络进程接收到响应头后根据 content-type: text-html 判断是 HTML 文件,浏览器为该请求创建一个渲染进程,渲染进程和网络进程间会建立一个共享数据的通道,网络进程往通道里放数据,渲染进程不断读取数据,并同,时将数据给 HTML 解析器,解析器动态解析数据,接收多少解析多少,而不是等整个文档加载完成后再解析的。

字节流转换为 DOM 需要三个阶段:

  • 第一个阶段通过分词器将字节流转换为 Token
  • 第二个和第三个阶段是同步进行的,需要将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中

HTML 解析器维护了一个 Token 栈结构,分词器将字节流转换为一个个 Token,Token 分为 TagToken 和 文本 Token,TagToken 又分为 StartTag 和 EndTag,具体规则如下:

  • 如果压入到栈中的是StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。
  • 如果分词器解析出来是文本 Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本 Token 是不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。
  • 如果分词器解析出来的是EndTag 标签,比如是 EndTag div,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。

通过分词器产生的新 Token 就这样不停地压栈和出栈,整个解析过程就这样一直持续下去,直到分词器将所有字节流分词完成。

虚拟 DOM

虚拟 DOM 是一个普通的 js 对象,是真实 DOM 的抽象表示。

特点:

  • 不是真实的浏览器 DOM 元素
  • 比真实 DOM 更轻量,操作更快
  • 使用 js 对象树来描述 DOM 结构
  • 减少直接操作真实 DOM 的次数

虚拟 DOM 工作原理

初始渲染:组件首次渲染时,创建整个 UI 的虚拟 DOM 树,将虚拟 DOM 树转换为真实 DOM 并渲染到页面

状态更新:状态变化后,重新创建整个组件的虚拟DOM树(新树),通过 diff 算法比较新旧两棵虚拟DOM树的差异,记录所有需要更新的节点,一次性将补丁应用到真实DOM上

为什么需要虚拟 DOM?

  • 真实 DOM 操作昂贵,每次操作都会触发重排/重绘,消耗大量资源
  • 在复杂应用中频繁操作 DOM 会导致性能下降
  • 使用虚拟 DOM,一次性更新 DOM,只触发一次重排

diff 算法