DOM
定义
-
DOM是JavaScript操作网页的接口,全称为“文档对象模型”(Document Object Model)。它的作用是将网页转为一个JavaScript对象,从而可以用脚本进行各种操作(比如增删内容)。 -
浏览器会根据
DOM模型,将结构化文档(比如HTML和XML)解析成一系列的节点,再由这些节点组成一个树状结构(DOM Tree)。所有的节点和最终的树状结构,都有规范的对外接口。 -
DOM只是一个接口规范,可以用各种语言实现。所以严格地说,DOM不是JavaScript语法的一部分,但是DOM操作是JavaScript最常见的任务,离开了DOM,JavaScript就无法控制网页。另一方面,JavaScript也是最常用于DOM操作的语言。后面介绍的就是JavaScript对DOM标准的实现和用法。
Node 节点
DOM的最小组成单位叫做节点(node)。文档的树形结构(DOM树),就是由各种不同类型的节点组成。每个节点可以看作是文档树的一片叶子。
节点的类型有七种
Document:整个文档树的顶层节点DocumentType:doctype标签(比如<!DOCTYPE html>)Element:网页的各种HTML标签(比如<body>、<a>等)Attribute:网页元素的属性(比如class="right")Text:标签之间或标签包含的文本Comment:注释DocumentFragment:文档的片段 浏览器提供一个原生的节点对象Node,上面这七种节点都继承了Node,因此具有一些共同的属性和方法。
查看节点类型
xxx.node.type
// 1 表示元素Element, 2 表示文本text, 8 表示comment注释
Node Tree 节点树
一个文档的所有节点,按照所在的层级,可以抽象成一种树状结构。这种树状结构就是DOM 树。它有一个顶层节点,下一层都是顶层节点的子节点,然后子节点又有自己的子节点,就这样层层衍生出一个金字塔结构,倒过来就像一棵树。
浏览器原生提供document节点,代表整个文档。
document
// 整个文档树
文档的第一层有两个节点,第一个是文档类型节点(<!doctype html>),第二个是 HTML 网页的顶层容器标签<html>。后者构成了树结构的根节点(root node),其他 HTML 标签节点都是它的下级节点。
除了根节点,其他节点都有三种层级关系。
- 父节点关系(
parentNode):直接的那个上级节点 - 子节点关系(
childNodes):直接的下级节点 - 同级节点关系(
sibling):拥有同一个父节点的节点DOM提供操作接口,用来获取这三种关系的节点。比如,子节点接口包括firstChild(第一个子节点)和lastChild(最后一个子节点)等属性,同级节点接口包括nextSibling(紧邻在后的那个同级节点)和previousSibling(紧邻在前的那个同级节点)属性。
Div完整原型链
代码
console.dir(div1)
- 存有自身属性:
className,id,style等等 - 第一层原型:
HTMLDivElement.prototype,存有所有div的共有属性 - 第二层原型:
HTMLElement.prototype,存有所有HTML标签的共有属性 - 第三层原型:
Element.prototype, 存有所有XML, HTML标签的共有属性 - 第四层原型:
Node.prototype, 存有所有Node共有的属性,Node节点包括XML标签文本注释,HTML标签文本注释等等 - 第五层原型:
EventTarget.prototype, 存有三个重要的函数,其中addEventLister比较重要 - 最后一层原型:
Object.prototype

总结
DOM很难用
DOM操作是跨线程
浏览器引擎
浏览器引擎分为渲染引擎和JS引擎
JS引擎不能操作页面,只能操作JS- 渲染引擎不能操作
JS,只能操作页面
那么这句代码是如何改变页面的呢?
document.body.appendChild(div1)
跨线程通信
- 首先浏览器发现
JS在body里面新增了div对象 - 然后浏览器就会通知渲染引擎也新增一个
div元素到页面里 - 根据
div对象渲染的div元素

插入新标签的完整过程
- 在
div1放入页面之前,所有对div1的操作都属于JS线程内的操作,浏览器一旦发现由JS引擎产生了变化,就会通知渲染线程在页面中根据div1对象在页面中渲染div1元素 - 把
div1放入页面后,所有对div1的操作都有可能触发重新渲染,例如div1.id = 'newID', 如果newID里有CSS样式,就会触发重新渲染。如果连续对div1多次操作,浏览器就有可能合并成一次操作。
属性同步
- 标准属性
修改div1的标准属性,将会被浏览器同步到页面中,例如id,className, title等等
data-*属性
修改div1的data-*属性,也会被浏览器同步到页面中
- 非标准属性
修改div1的非标准属性,将只会停留在JS线程中,不会同步到页面。例如这里的X属性
注意事项
如果用户由自定义属性,又想触发页面的更新,可以使用data-作为前缀

PropertyVSAttribute
Property属性
JS线程中div1的所有属性, 称为Property
Attribute属性
渲染引擎中的div1对应标签的属性,称为Attribute
- 二者区别
- 一般情况下,同名的
property和attribute相等 - 如果不是标准属性,二者在开始的时候相等,一旦
property改变,attribute将会维持原先的值,所以二者会不相等 attribute只支持字符串property支持字符串,布尔等类型
- 一般情况下,同名的
获取元素
window.id(xxx)或者直接id(xxx)document.getElementById('id(xxx)')document.getElementsByTagName('div')[0]document.getElementsByClassName('red')[0]document.querySelctor('#idxxx')document.querySelctorAll('.red')[0]
注意事项
- 推荐使用
querySelector或者querySelctorAll - 不推荐使用
getElementsByxxx, 除非需要兼容IE - 做测试,个人项目,可以使用
id(xxx)
获取特定元素
获取的元素就是一个对象
- 获取
html元素
document.documentElement
- 获取
head元素
document.head
- 获取
body元素
document.body
- 获取窗口(窗口不是元素)
window
- 获取所有元素
// IE发明的,只在IE浏览器为真值,其他浏览器为falsy值
document.all
增加
创建标签节点
document.createElement('style')
document.createElement('script')
document.createElement('li')
let div1 = document.createElement('div')
创建文本节点
text1 = document.createTextNode('你好')
标签插入文本
div1 = appendChild(text1)
div1.innerText = '你好' // 或者
div1.textContent = '你好'
// 注意,以下为错误操作, 因为'你好'不是Node节点
div1.appendChild('你好')
插入页面中的注意事项
- 需要把创建的标签插入到浏览器中
- 需要插入到
head或者body,才会生效,代码为document.body.appendChild(div)
关于appendChild
代码:页面中有div #test1 和 div #test2
let div = document.createElement('div')
test1.appenChild(div)
test2.appenChild(div)
问题1: 请问div最终出现在哪里?test1? test2? 二者都在?
答案:test 2里面,因为一个元素不能出现在两个地方,除非复制一份,代码为let div2 = div1.cloneNode()。
删除
方法
- 旧的方法:
parentNode.removeChild(childNode) - 新的方法:
childNode.remove(), 但是IE不兼容
问题
请问,如果一个node被移除了DOM树,还能被复原回来吗?
答案:可以的。如果想完全移除,可以这样写div.remove(), div = null
修改
修改标准属性
- 修改
id
div1.id = 'div2'
- 修改
class
div.className= 'bule' // 这个会覆盖之前的className, 如果前面的className 不能变的时候,会造成Bug 推荐写法为
div.className += ' yello' // 这样就保留前面的className, 并同时添加新的className, 或者使用classList
div.classList.add('red')
- 修改
style
div.style = 'width: 100px; color:bule;'
div.style.backgroundColor 等价于 div.style.bakcground-color // 前面的为正确语法,后面的会报错
div.style.width = '200px'
- 修改
data
div.dataset.x = 'frank'
读取标准属性
- 读取
class
div.classList
或者
div.getAttribute('class')
- 例外情况
<a id = test href="/xxx">/xxx</a>
********
console.log(test.href)
// 结果为"htttp://www.baidu.com/x",不能找到正确的路径
console.log(test.getAttribute('href')
// 结果为"/xxx",这种方法可以找到正确的路径
修改事件处理函数
- 修改
div.onclick
div.onclick默认值为null,默认点击div不会发生任何事件,如果把div.onclick修改为一个函数fn, 点击div的时候,就会调用fn。
div1.onclick = ()=>{console.log('hi'}
// 事实上的代码为
div1.onclick.call(div1, event)
其中fn.call(div, event)的div会当成this, event包含了点击事件的所有信息,例如坐标等等
- 修改
div.addEventListener
div.addEventLister可以添加多个函数给一个元素中,是div.onclick的加强版
修改内容
修改文本内容
div.innerText = 'xxx'
div.textContent = 'xxx'
修改HTML内容
div.innerHTML = '<span>信息</span>' // 但是有限制字符
修改标签
div.innerHTML = '' // 先清除
div.appendChild(div2) // 再添加内容
修改父节点 将原先的子节点移动到新的父节点里面
newParent.appendChild(div)
查看
查看父节点
node.parentNode
// 或者
node.parentElement
查看祖先节点
node.parentNode.parent.Node
查看子节点
node.childNodes // 不推荐使用这个,回车也会算作节点
node.children // 推荐使用这个
当子节点发生变化的时候,两者都会更新子节点的信息。
查看同辈节点
- 查看同辈所有节点
node.parentNode.childNode // 排除自己本身
node.parentNode.children // 排除自己本身
- 查看老大(第一个节点)
node.firstChild
- 查看老小(最后一个节点)
node.lastChild
- 查看上一个哥哥/姐姐(上一个节点)
node.previousSibling
- 查看下一个弟弟/妹妹(下一个节点)
node.nextSiblings
查看div里面所有的元素
travel = (node, fn) => {
fn(node)
if(node.children){
for(let i = 0; i < node.children.length ; i++){
travel(node.children[i], fn)
}
}
}
travel(div1, (node)=>console.log(node))
更多信息
But why's the browser DOM still so slow after 10 years of effort?