背景
DOM 是 JavaScript 操作网页的接口,全称为“文档对象模型”(Document Object Model)。它的作用是将网页转为一个JavaScript对象,从而可以用脚本进行各种操作(比如增删内容)。
浏览器会根据 DOM 模型,将结构化文档(比如HTML和XML)解析成一系列的节点,再由这些节点组成一个树状结构(DOM Tree)。所有的节点和最终的树状结构,都有规范的对外接口。
DOM 只是一个接口规范,可以用各种语言实现。所以严格地说,DOM 不是 JavaScript 语法的一部分,但是 DOM 操作是 JavaScript 最常见的任务,离开了 DOM,JavaScript 就无法控制网页。
节点
DOM 的最小组成单位叫做节点(node)。文档的树形结构(DOM 树),就是由各种不同类型的节点组成。每个节点可以看作是文档树的一片叶子。
Document:整个文档树的顶层节点
DocumentType:doctype标签(比如<!DOCTYPE html>)
Element:网页的各种HTML标签(比如<body>、<a>等)
Attribute:网页元素的属性(比如class="right")
Text:标签之间或标签包含的文本
Comment:注释
DocumentFragment:文档的片段
浏览器提供一个原生的节点对象Node,上面这七种节点都继承了Node,因此具有一些共同的属性和方法。
注意p标签中的文字也是一个节点,节点是包含元素的
节点树
- 浏览器原生提供document节点,代表整个文档。
- 文档的第一层有两个节点,第一个是文档类型节点(<!doctype html>),第二个是 HTML 网页的顶层容器标签。后者构成了树结构的根节点(root node),其他 HTML 标签节点都是它的下级节点。
具体语法
获取ID(假设ID是xxx)
- window.xxx(只要ID和全局属性不冲突就可以用这种方法)
- xxx
- document.getElementById('xxx')
- document.getElementsByTagName('div')[0](找到所有标签名为div的元素得到一个伪数组)
- document.getElementsByClassName('red')[0](类名)
- document.querySelector('#xxx')(满足条件第一个)
- document.querySelectorAll('.red')[0]
- querySelector里面借用了css选择器非常地方便
获取特定元素
- 获取html元素 document.documentElement
- 获取head元素 document.head
- 获取body元素 document.body
- 获取窗口(窗口不是元素) window
- window虽然不是一个元素但我们可以监听
- 获取所有元素 document.all
- 这个document.all,第6个falsy值
- 分清楚根节点和所有节点的区别,获取根节点还需要展开才是所有节点
div元素的六层原型链
- 自身属性: className、id、style 等等通过构造函数this.xxx写上去的
- 第一层原型HTMLDivElement.prototype:这里面是所有div元素的共有的属性
- 第二层原型HTMLElement.prototype:这里面是所有HTML元素共有的属性
- 第三层原型Element.prototype:这里面是所有XML、HTML元素的共有属性
- 第四层原型Node.prototype:这里面是所有节点共有的属性,节点包括XML标签文本注释、 HTML标签文本注释等等
- 第五层原型EventTarget.prototype:里面最重要的函数属性是addEventListener
- 最后一层原型就是Object.prototype了
节点的增删改查
节点是包含元素和文本等在内的,不要认为元素就是所有得节点,所有DOM节点对象都继承了 Node 接口,拥有一些共同的属性和方法。这是 DOM 操作的基础。
增
- 建一个标签节点
- document.createElement('标签名')
- let div1 = document.createElement('div')
- 你创建的标签默认处于JS线程中,内存中
- 创建一个文本节点
- text1 = document.createTextNode("你好")
- 标签里面插入文本
- div1. appendChild(text1) 必须加文本节点的名字,不可以加文本的内容
- div1.innerText='你好' (Node原型提供)
- div1.textContent ='你好'(Element原型提供)
- 把新创建的标签插入页面中
- 必须把我们刚刚创建在js线程中的标签插到已在页面里的标签里面,它才会生效
- head或者body,document.body.appendChild(div1)
- 或者已在页面中的元素.appendChild(div1)
- appendChild() 方法
- appendChild() 方法可向节点的子节点列表的末尾添加新的子节点。
- node.appendChild(node)
- 相同的子节点不可以出现在两个地方:如果同时把一个子节点又插入节点1,又插入节点2 ,那该子节点最终会在节点2中
- 除非你把这个子节点复制一份div2 = div1.cloneNode(),如果传递给它的参数是 true,它还将递归复制当前节点的所有子孙节点。否则,它只复制当前节点。
删
- 旧方法节点提供的: parentNode.removeChild(childNode)
- 找到要删除的节点的父节点,用父节点的removeChild删除
- 新方法: childNode.remove()
- 直接删除
- 如果一个node被移出页面(DOM树),那么它还可以再次回到页面中(appendChild())。因为他只是从页面中被删除了,他还在js引擎线程,可以使节点=null来使这块内存回收掉
改
1、改节点的标准属性
(1)改class
- div.class = 'red'是不行的但是id是可行的,一般情况下是不能让js保留字做key的
- div.className= 'red blue' (全覆盖)
- div.className += 'black' (增加)
- div.classList.add('black')(增加)
(2)改style
- div.style = 'width: 100px; color: blue;'(全覆盖)
- 改style的一部分: div.style.width = '200px'
- 大小写代替css里的中划线: div.style.backgroundColor = 'white'
(3)改data-*属性: div.dataset.x = 'frank'(没人用了)
(4)读标准属性
- div.属性名 / a.href(会给你把路径补充完整)
- div.getAttribute('class') / a.getAttribute('href')(原封不动的给你)
- 两种方法都可以,但值可能稍微有些不同
2、改节点的事件处理函数
- div.onclick默认为null,默认点击div不会有任何事情发生
- 但是如果你把div.onclick改为一个函数fn,那么点击div的时候,浏览器就会调用这个函数并且是这样调用的fn.call(div, event),div会被当做this,event则包含了点击事件的所有信息,如坐标
- div.addEventListener,是div.onclick的升级版
3、改子节点
(1)改文本内容
- div.innerText = 'xxx'
- div.textContent= 'xxx'
- 两者几乎没有区别
(2)改HTML内容
div.innerHTML = '重要内容'
(3)改标签
div.innerHTML = ''//先清空
div.appendChild(div2) //再加内容
4、改父节点
- 想找个新爸爸,让新爸爸把自己加进去就完事了
- newParent.appendChild(div)
- 直接这样就可以了,直接从原来的地方消失
查
查节点
- 查自己
node
- 查爸爸
- node.parentNode
- node.parentElement
- 查爷爷
node.parentNode.parentNode
- 查子代
(1)所有子代
- node.childNodes(会把你不想要的空格也当成一个文本子节点)(节点提供的)
- node.children(这个不会,只是元素节点,优先使用)(元素提供的)
- 当子代变化时,两者也会实时变化
- document.querySelectorAll('.red')[0]不会实时更新
(2)一个子代
- node.children[0]
- 查看老大node.firstChild
- 查看老幺node.lastChild
- 查兄弟姐妹 (1)查所有兄弟姐妹
- node.parentNode.childNodes但自己也在里面,要排除自己和所有文本节点
- node.parentNode.children但自己也在里面,要排除自己
- 排除自己:要用for循环遍历所有子节点,然后把自己排除出去
let siblings = []
let arr = div2.parentElement.children //找出所有子节点形成一个数组
for(let i=0;i<c.length;i++){ //遍历数组中每个元素
if(arr[i] !== div2){ //如果不是自己
sibling.push(arr[i]) //就加入到新数组中
}
}
- 查看上一个哥哥/姐姐node.previousSibling(节点兄弟,可能为文本节点)
- 查看上一个哥哥/姐姐node.previousElementSibling(元素兄弟)
- 查看下一个弟弟/妹妹node.nextSibling(节点兄弟,可能为文本节点)
- 查看下一个弟弟/妹妹node.nextElementSibling(元素兄弟)
- 查看一个节点里所有的元素(儿子、孙子、曾孙子,,,)
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))
DOM操作是跨线程的
- JS引擎不能操作页面,只能操作JS
- 渲染引擎不能操作JS,只能操作页面
- 怎么通过js来改变渲染呢,这就用到了跨线程通信
- document.body.appendChild(div1)---这是一句跨线程的DOM操作
- 当浏览器发现JS在body里面加了个div1对象
- 浏览器就会通知渲染引擎在页面里也新增一个div元素
- 新增的div元素所有属性都照抄div1对象
插入新标签的完整过程
- 在div1放入页面之前,你对div1所有的操作都属于JS线程内的操作
- 把div1放入页面之时
document.body.appendChild(div1)浏览器会发现JS的意图,就会通知渲染线程在页面中渲染div1对应的元素 - 把div1放入页面之后你对div1的操作都有可能会触发重新渲染
- 如果你连续对div1多次操作,浏览器可能会合并成一次操作,也可能不会,如果连续两次对宽度进行变化会变成一个但是在中间加上.clientWidth(获取宽度不先渲染一个怎么获取?)就会分开渲染
- div1.id = 'newid'可能会重新渲染,也可能不会,如果ID关联着css里面的选择器就会重新渲染
- div1.title= 'new'可能会重新渲染,也可能不会
属性同步
- 标准属性:对div1的标准属性的修改,会被浏览器同步到页面中比如id、className、 title 等
- data-*:属性同上
- 非标准属性:对非标准属性的修改,则只会停留在JS线程中,不会同步到页面里比如x属性
- 如果你有自定义属性,又想被同步到页面中,请使用data-作为前缀
Property V.S. Attribute
- property属性:JS线程中div1对象的所有属性,叫做div1的property,是property是DOM中的属性,是JavaScript里的对象;
- attribute也是属性:渲染引擎中div1元素的属性,叫做attribute,attribute是HTML标签上的特性,它的值只能够是字符串;
区别
- 大部分时候,同名的property和attribute值相等
- 但如果不是标准属性,那么它俩只会在一开始时相等(把div1放入页面之后只有标准属性会同步)
- 注意attribute的值只支持字符串,而property的值支持字符串、布尔等类型
补充
- 用DOM获取到的HTML属性在js中也是一个对象
- 属性本身是一个对象(Attr对象),但是实际上,这个对象极少使用。一般都是通过元素节点对象来操作属性。
- style不仅可以使用字符串读写,它本身还是一个对象,可以直接读写个别属性。