DOM,相信无论是初入前端的小伙伴还是已经沉浸在前端多年的大佬,都对这个东西不会很陌生,但是对于这个东西很多人只知其然不知其所以然,所以今天我们来刨析一个这个跟多我打了很多年交道的老朋友。
一、DOM 节点的基本概念
1.1 DOM 节点的基本概念
DOM
,英文全称是[Document Object Model
],翻译过来就是文档对象模型 ,DOM节点是构成网页文档结构的基本单位,是 HTML/XML
文档中每个组成部分的对象表示,浏览器通过 DOM
节点构建文档的树形结构 DOM 树。
🧩 DOM节点就像乐高积木,想象你在玩乐高玩具:
- 每个单独的乐高积木块就是一个DOM节点
- 你把积木拼成房子、车子,就像DOM节点组成网页
- 大积木可以套小积木,就像
<div>
里面可以放<p>
1.2 DOM主要节点类型
在 DOM
中,nodeType
的值用于区分不同类型的节点。这些值是预定义的常量,可以帮助开发者在操作 DOM
时准确地识别和处理不同类型的节点。
类型 | 值 | 说明 |
---|---|---|
元素节点 | 1 | HTML 标签(如 <div> 、<p> ) |
属性节点 | 2 | HTML 属性(如 class="title" ) |
文本节点 | 3 | 元素内的文本内容 |
注释节点 | 8 | HTML 注释(<!-- 注释 --> ) |
文档节点 | 9 | 整个文档(document ) |
文档类型节点 | 10 | <!DOCTYPE html> |
- 元素节点(Element Node)
- 值:1
- 说明 :表示HTML标签,如
<div>
、<p>
等。- 用途 :当你需要操作HTML元素(如获取元素的属性、修改元素的内容等)时,可以通过检查节点类型是否为1来确定它是一个元素节点。
- 属性节点(Attribute Node)
- 值:2
- 说明 :表示HTML属性,如
class="title"
。- 用途 :虽然在现代DOM操作中,属性通常通过元素的
attributes
属性来访问,但在某些情况下,你可能需要检查节点类型是否为2来确定它是一个属性节点。
- 文本节点(Text Node)
- 值:3
- 说明:表示元素内的文本内容。
- 用途:当你需要获取或修改元素内的文本内容时,可以通过检查节点类型是否为3来确定它是一个文本节点。
- 注释节点(Comment Node)
- 值:8
- 说明 :表示HTML注释,如
<!-- 注释 -->
。- 用途:在某些情况下,你可能需要处理HTML注释,例如在解析或生成HTML代码时,可以通过检查节点类型是否为8来确定它是一个注释节点。
- 文档节点(Document Node)
- 值:9
- 说明 :表示整个文档,即
document
对象。- 用途:当你需要操作整个文档(如获取文档的根节点、设置文档的标题等)时,可以通过检查节点类型是否为9来确定它是一个文档节点。
- 文档类型节点(Document Type Node)
- 值:10
- 说明 :表示文档类型声明,如
<!DOCTYPE html>
。- 用途 :在某些情况下,你可能需要检查文档类型声明,例如在解析或生成HTML代码时,可以通过检查节点类型是否为10来确定它是一个文档类型节点。
1.3 DOM 节点的核心属性
在DOM
中,所有节点都具有一些通用属性,这些属性可以帮助我们获取节点的基本信息和关系。
属性 | 说明 |
---|---|
nodeName | 节点名称(标签名大写) |
nodeType | 节点类型(数字值) |
nodeValue | 节点值(文本/注释节点才有) |
childNodes | 所有子节点的集合 |
parentNode | 父节点 |
previousSibling | 前一个兄弟节点 |
nextSibling | 后一个兄弟节点 |
1 nodeName
说明 :节点名称。对于元素节点,它是标签名的大写形式;对于属性节点,它是属性名;对于文本节点,它是
#text
;对于注释节点,它是#comment
;对于文档节点,它是#document
。用途 :通过
nodeName
可以快速获取节点的类型或名称。
2 nodeType说明 :节点类型,是一个数字值,表示节点的类型。常见的节点类型及其值如
1.2
所述用途 :通过
nodeType
可以判断节点的类型,从而进行相应的操作。
3 nodeValue说明 :节点的值。对于文本节点,它是文本内容;对于注释节点,它是注释内容;对于属性节点,它是属性值。其他类型的节点
nodeValue
通常为null
。用途 :通过
nodeValue
可以获取或设置节点的值。
4 childNodes说明 :一个
NodeList
对象,包含当前节点的所有子节点。用途 :通过
childNodes
可以遍历当前节点的所有子节点,进行操作或查询。
5 parentNode说明 :当前节点的父节点。如果当前节点是文档的根节点,则
parentNode
为null
。用途 :通过
parentNode
可以获取当前节点的父节点,从而进行向上级的查询或操作。
6 previousSibling说明 :当前节点的前一个兄弟节点。如果没有前一个兄弟节点,则为
null
。用途 :通过
previousSibling
可以获取当前节点的前一个兄弟节点,进行同级的查询或操作。
7 nextSibling说明 :当前节点的后一个兄弟节点。如果没有后一个兄弟节点,则为
null
。用途 :通过
nextSibling
可以获取当前节点的后一个兄弟节点,进行同级的查询或操作。
1.4 DOM 节点的操作
我们在讲
DOM
节点的类型时候讲过类型的值为9
的节点叫文档节点(Document Node) ,document
是DOM
中的一个核心对象,它代表了整个HTML
文档。通过document
对象,可以访问和操作文档中的所有元素和属性。它是整个DOM树
的根节点,是与页面内容交互的入口点。
方法 | 描述 |
---|---|
getElementById() |
通过 ID 获取单个元素 |
querySelector() |
通过 CSS 选择器获取第一个匹配元素 |
querySelectorAll() |
通过 CSS 选择器获取所有匹配元素 |
createElement() |
创建新的 HTML 元素 |
createTextNode() |
创建文本节点 |
createDocumentFragment() |
创建文档片段(性能优化用) |
write() |
向文档流写入内容 |
addEventListener() |
添加事件监听器 |
removeEventListener() |
移除事件监听器 |
hasFocus() |
检查文档是否获得焦点 |
关于DOM节点的操作我将列举其主要函数等,但是并不会深入探讨,现在前端的主流框架VUE等是数据驱动视图 ,通过
document
直接操作DOM违背了Vue 的设计原则。
现在框架中为什么不推荐直接操作 DOM?
-
违背响应式原则
Vue 的响应式系统会自动跟踪数据变化并更新 DOM,直接操作 DOM 会导致视图与数据状态不同步。
-
破坏组件封装性
直接操作其他组件的 DOM 可能引发不可预期的副作用,降低代码可维护性。
-
性能优化失效
Vue 的虚拟 DOM(Virtual DOM)会高效批量更新真实 DOM,直接操作会绕过这一优化机制。
-
SSR/跨平台兼容性问题
服务端渲染(SSR)或非浏览器环境(如 Weex)中 document 对象不存在,直接操作会导致错误。
-
DOM 操作是昂贵的
每次修改 DOM(如修改样式、添加/删除节点),浏览器需要重新计算布局并重新绘制,这会消耗大量 CPU/GPU 资源。
html
<template>
<div ref="myDiv">Hello Vue</div>
</template>
<script>
export default {
mounted() {
// 通过 $refs 访问而不是 document
this.$refs.myDiv.textContent = 'Updated content';
}
}
</script>
二、VNode
2.1 什么是 VNode?
VNode
全称为[Virtual Node
],中文名称虚拟节点 ,是 Vue
用来描述真实 DOM 节点的 JavaScript 对象
。它相当于真实 DOM
的轻量级"蓝图 "。你可以把VNode想象成建筑师的"设计图纸 ",而真实DOM
就是实际建好的房子。
2.2 VNode 的核心属性
属性 | 类型 | 说明 |
---|---|---|
tag |
String |
HTML标签名或组件名 |
data |
Object |
包含class , style , attrs 等 |
children |
Array |
子VNode数组 |
text |
String |
文本节点的内容 |
elm |
DOM Element |
对应的真实DOM节点 |
key |
String/Number |
用于Diff算法的唯一标识 |
-
tag
- 类型:
String
- 说明:
表示 HTML 原生标签名(如"div"
、"span"
)或注册的组件名(如"MyComponent"
)。- 原生标签会渲染为对应的 DOM 元素
- 组件名会触发组件实例化流程
- 类型:
-
data
-
类型:
Object
-
说明:
包含节点的配置数据,常用字段包括:javascript{ class: 'active', // CSS 类名 style: { color: 'red' }, // 行内样式 attrs: { id: 'app' }, // HTML 特性 on: { click: handler } // 事件监听 }
-
-
children
- 类型:
Array<VNode>
- 说明:
当前节点的子节点数组,支持嵌套结构。特殊说明:- 空数组表示无子元素
- 文本节点可用字符串直接表示(如
['文本']
)
- 类型:
-
text
-
类型:
String
-
说明:
专为文本节点设计的属性,与tag
互斥。例如:javascript{ text: '纯文本内容' } // 等效于 document.createTextNode()
-
-
elm
- 类型:
DOM Element
- 说明:
在 patch 阶段由框架自动挂载,指向该 VNode 对应的真实 DOM 节点。开发者通常无需手动操作。
- 类型:
-
key
- 类型:
String | Number
- 说明:
Diff 算法的核心优化标识,适用于:v-for
列表渲染(避免就地复用问题)- 动态组件切换(强制触发生命周期)
- 类型:
2.3 为什么要使用VNode
-
性能优化
- 减少直接操作DOM的次数
直接操作DOM会触发浏览器重排和重绘,性能消耗较大。VNode通过在内存中操作虚拟DOM,批量处理真实DOM更新,减少渲染开销。 - 高效的更新机制
通过Diff算法(如Vue的Snabbdom)对比新旧VNode差异,仅更新必要的DOM节点,避免全量渲染。
- 减少直接操作DOM的次数
-
提高开发效率
- 声明式编程
用声明式描述UI状态(如Vue模板/React JSX),替代手动操作DOM的命令式代码,提升可维护性。 - 组件化开发
支持将UI拆分为可复用组件(如.vue文件/React组件),简化复杂界面开发。
- 声明式编程
-
跨平台支持
- 跨浏览器兼容
基于JavaScript标准实现,不依赖特定浏览器API,兼容Chrome/Firefox/Safari等。 - 跨平台扩展
虚拟DOM可应用于:- 服务端渲染(SSR):如Nuxt.js/Next.js
- 移动端:React Native/Weex
- 桌面端:Electron
- 跨浏览器兼容
特性 | 设计图纸(VNode) | 真实房子(DOM) |
---|---|---|
表示方式 | 用纸上的线条和标注表示房子 | 是实际建好的砖瓦结构 |
修改难度 | 修改起来非常快(擦掉重画就行) | 修改代价高(拆墙重建很费劲) |
成本 | 成本低(就是一张纸) | 成本高(要用真实建筑材料) |
性能优化 | 减少直接操作DOM的次数,高效的更新机制 | 直接操作DOM,性能较低 |
开发效率 | 声明式编程,组件化开发,代码更简洁、易于维护 | 手动操作DOM,代码复杂,维护成本高 |
跨平台支持 | 跨浏览器兼容,支持服务器端和移动设备 | 依赖于浏览器,不支持跨平台 |
2.4 VNode与DOM 的关系
VNode
本质是一个普通的 JavaScript
对象,用来描述 DOM节点
,它的作用是VUE
在内存中维护一个虚拟 DOM树
(由多个 VNode
组成),用于高效计算 DOM
的更新。
javascript
// 一个简单的 VNode 示例
{
tag: 'div', // 标签名
props: { class: 'container' }, // 属性(如 class, id)
children: [ // 子节点
{ tag: 'p', children: 'Hello World' }
]
}
// 对应的真实DOM节点
<div class="container">
<p>Hello World</p>
</div>
三、VNode 的创建过程
3.1 VNode 如何变成真实 DOM
Vue 的渲染流程:
-
模板编译:Vue 模板(如 .vue 文件)会被编译成 渲染函数(render function)。
-
生成 VNode:渲染函数执行后,返回一个 VNode 树(虚拟 DOM)。
-
Diff 比对:当数据变化时,Vue 会生成新的 VNode,并和旧的 VNode 进行对比(Diff 算法)。
-
更新 DOM :只修改真正变化的部分(避免全量更新 DOM,提高性能)。
3.2 手动创建
在深入探讨之前,我们需要先了解 Render 函数
的概念。Render 函数
是一个接收 createElement
方法(通常简写为 h
)作为参数的函数,它返回一个虚拟 DOM 节点(VNode),用于描述组件的渲染结构。
createElement
(即 h
函数)是 Vue 渲染机制的核心,专门用于创建 VNode。其有三个接收参数标签名(必需)
、数据对象(可选)
、子节点(可选)
基本语法如下:
javascript
// HTML 标签
createElement('div')
// 组件
createElement(MyComponent)
// 文本子节点
createElement('div', 'hello world')
// 数组子节点
createElement('div', [
createElement('span', 'hello'),
createElement('span', 'world')
])
关于 Render 函数与 createElement 相关的知识点也是非常的密集和重要,因此放在同一个篇章肯定是讲不完了,所以后续会详细的讲解一些关于这两个的使用。