概述
我们可以把网页看做成一棵树, JavaScript 是通过 document 来操作这棵树的。
JavaScript 用window.document操作网页。
- 在浏览器控制台输入
window.document会得到一个:#document文档,可以拿到根节点,根节点中包含其他节点 window.document可以获取得到网页所有的元素
DOM (Document Object Model)全称是“文档对象模型”。是 JavaScript 操作网页的接口。作用把网页转成一个 JavaScript 对象,从而可以用脚本对其进行操作(例如增删内容)。
节点
DOM 的最小组成单位叫做节点(node)。文档的树形结构(DOM 树),就是由各种不同类型的节点组成。
节点有以下类型:
Document:整个文档树的顶层节点DocumentType:doctype标签(比如<!DOCTYPE html>)Element:网页的各种HTML标签(比如<body>、<a>等),也就是元素Attr:网页元素的属性(比如class="right")Text:标签之间或标签包含的文本Comment:注释DocumentFragment:文档的片段
浏览器提供一个原生的节点对象Node,上面这七种节点都继承了Node,因此具有一些共同的属性和方法。
节点树
一个文档的所有节点,按照所在的层级,可以抽象成一种树状结构。这种树状结构就是 DOM 树。它有一个顶层节点,下一层都是顶层节点的子节点,然后子节点又有自己的子节点,就这样层层衍生出一个金字塔结构,又像一棵树。
浏览器原生提供document节点,代表整个文档。
文档的第一层有两个节点,第一个是文档类型节点(<!doctype html>),第二个是 HTML 网页的顶层容器标签<html>。后者构成了树结构的根节点(root node),其他 HTML 标签节点都是它的下级节点。
除了根节点,其他节点都有三种层级关系。
- 父节点关系(parentNode):直接的那个上级节点
- 子节点关系(childNodes):直接的下级节点
- 同级节点关系(sibling):拥有同一个父节点的节点
DOM 提供操作接口,用来获取这三种关系的节点。比如,子节点接口包括firstChild(第一个子节点)和lastChild(最后一个子节点)等属性,同级节点接口包括nextSibling(紧邻在后的那个同级节点)和previousSibling(紧邻在前的那个同级节点)属性。
获取元素(标签)
(获取所得到的都是对象)
- 获取标签名为 id 的元素
window.id 或者直接输入 id
或者
document.querySelector('#id')
如果想获取的 id 和 window 的全局属性冲突,第一种方式就不行了
- 获取标签名为 div 的元素
document.querySelectorAll('div')[0]
注意找到的是一个数组,所以要写明下标,比如获取第一个加 [0]
想操作必须加下标,不加属于是遍历这个元素,不能操作
- 获取 html 元素
document.documentElement
- 获取 head 元素
document.head
- 获取 body 元素
document.body
- 获取窗口
window
window虽然不是一个标签,但是有时候想要获取window添加一些事件监听什么的
- 获取全部元素
document.all
在所有浏览器上直接用 document.all 没有什么问题。但如果做一个判断,非IE浏览器会认为 document.all是一个假值。
所以可以通过下面语句判断是否为IE
获取html标签和获取所有标签的区别
html 只是一个根元素,获取其他元素还要展开才有;
获取所有元素标签指获取html、body、head、div等所有元素,一字排开全部展示
节点的增删改查
增
创建一个标签节点
let div1 = document.createElement('div')
let div1 = document.createElement('style')
let div1 = document.createElement('script')
let div1 = document.createElement('li')
...
在标签里插入文本
(两种方式)
第一种:先创建再插入(这是node提供的接口)
let text1 = document.createTextNode('你好')
div1.appendChild(text1)
第二种:直接插入(这是element提供的接口)
div1.innerText = '你好'
(或者)
div1.textContent = '你好'
在页面显示
- 自己创建的标签默认处于 JS 线程中,不会显示在页面里。必须把它插入到 head 或者 body 里才能生效。
document.body.appendChild(div1)
例如增加一个节点并在页面显示:
let div1 = document.createElement('div');
div1.innerText = '你好';
document.body.appendChild(div1);
div1.style.position = 'fixed';
div1.style.top = 0;
div1.style.left = 0;
div1.style.color = 'red';
div1.style.fontSize = '100px';
div1.style.background = 'white'
- 一个元素不能出现在两个地方,例如创建一个DIV,先插入到 test1 里面,再插入到 test2 里,那么最终会出现在 test2 里。
删
childNode.remove()
- childNode 表示要删除的那个 node
- 如果一个 node 被移除了页面,再次添加是可以被加回来的。如果要彻底删除,再输入
childNode = null就好了。
例如:
let div1 = document.createElement('div') #创建节点 div1
div1.innerText = '你好' #在 div1 中增加文本 你好
document.body.appendChild(div1) #将 div1 添加到页面中
div1.remove() #从页面中移除 div1
div1 = null #彻底删除节点 div1
改
改属性 class
- 增加属性 :
div1.className = 'aaa' - 修改属性 :
1 . 直接用添加的方法会覆盖掉原有属性
div1.className = 'bbb'
div1.className = 'bbb aaa'
...
2 . 在原有属性中添加新属性(两种)
div1.className += 'bbb'
div1.classList.add('bbb')
改样式 style
- 全改 :
div.style = 'xxx'例如:div1.style = 'width:100px; color:biue;' - 改某个 :
div.style.xxx = 'yyy'例如:div1.style.width = '100px'
改 data-* 属性
div1.dataset.x = 'bbb'
(改自定义属性,一般作为库开发用)
div1.setAttribute('data-x','test') #在 div1 上加一个 data-x ,它的值是 test
div1.getAttribute('data-x') #获取 data-x
div1.dataset.x #获取 data-x
div1.dataset.x = 'bbb' #改 data-x
读标准属性
- 两种方法,第二种更安全点
第一种:
div1.style
div1.className
div1.id
...
第二种:
div1.getAttribute('aaa')
例如:
打印出的两种结果不一样
改事件处理函数
改内容
- 改文本内容 (两种)
div1.innerText = 'xxx'
div1.textContent = 'xxx'
- 改HTML内容 (修改的HTML内容不易太大,运行容易卡爆)
div1.innerHTML = 'xxx'
- 改标签
div1.innerHTML = '' #先清空标签
div1.innerHTML = '<strong>重要</strong>'
- 改父节点
newParent.appendChild(div1)
查
查父节点
div1.parentNode
(或)
div1.parentElement
查父节点的父节点
div1.parentNode.parentNode
查子节点
div1.childNodes
(或)
div1.children
- 用
childNodes查结果会包含文本节点,用children不会;由于childNodes会查到不需要的节点,一般优先使用children - 当子节点发生变化。这两者会自动更新
- 查第一个子节点
div1.firstChild
- 查最后一个子节点
div1.lastChild
查同级节点
- 查除自己外其他同级节点 (先获取父结点的子结点;再排除自己,得到同级节点)
div1.parentElement.children;
let siblings = [];
let yyy = div1.parentElement.children
for(let i=0; i<yyy.length; i++){
if(yyy[i] !== div1){
siblings.push(yyy[i])
}
};
siblings;
(一般不用 childNodes ,childNodes 包含文本节点排除起来比较麻烦)
- 查前一个节点
div1.previousSibling
- 查后一个节点
div1.nextSibling
(直接用 previousSibling 或者 nextSibling 可能会查到文本节点。想避开其他只查元素节点加一个 Element。例如:div1.previousElementSibling和 div1.nextElementSibling)
遍历一个 div 中所有元素
travel = (node, fn) => { //travel接受一个节点和一个函数
fn(node) //函数调用这个节点;此时这个函数未定义
if(node.children){ //如果节点有子元素,遍历这个子元素
for(let i=0; i<node.children.length; i++){
travel(node.children[i], fn) //每一个子元素再次teavel,再走一遍这个流程
}
}
};
travel(div1, (node) => console.log(node)) //函数fn在此时定义,即:console.log(node)
DOM的跨线程操作
浏览器分有渲染引擎和 JavaScript 引擎,渲染引擎负责渲染页面,JS引擎负责执行JS代码。两个线程各司其职,只做自己的工作。document.body.appendChild(div1)这句 js 是如何改变页面的?
答案是通过跨线程通信完成。
当浏览器发现 JS 在 body 里加了一个 div1 对象,浏览器就会通知渲染引擎在页面里也增加一个div元素,渲染引擎会开始渲染,新的div元素所有属性都照抄div1对象。如下图示:
- 插入新标签的完整过程
- 在div1放入页面之前
我们对div1的所有操作都只在JS线程里
- 在div1 放入页面之时
浏览器会发现JS的意图,并通知渲染线程在页面中渲染div1对应的元素
- 在div1放入页面之后
我们对 div1 的操作有可能会触发重新渲染,比如改属性, div1.id = 'newid', div1.title = 'new',也可能不会触发。如果对 div1进行多次操作,浏览器有可能会合并成一次操作。
比如下面的例子:
做一个动画,让 test 宽度从100变成200
先给 test 加一个‘start’类,再加一个‘end’类。发现没有动画效果 因为浏览器发现在短时间内对 classList 进行了两次操作,它会合并成一次。动画效果就没有了
加 test.clienWidth 后阻止了合并,因为在先给 test 加一个‘start’类后,要获取一下 test.clienWidth 。所以浏览器必须先立刻渲染第一次,获取客户端宽度后再渲染第二次。所以虽然 test.clienWidth 没有啥效果,但阻止了合并。
- 属性同步
我们在JS里修改了 div1 的属性,那渲染线程里的div1属性会自动同步吗?
- 标准属性会同步过去
- data-* 属性会同步过去
- 自定义属性或者非标准属性不会同步过去。(如果想要自定义属性同步,需要用
data-做前缀)
- Property 与 Attribute
- JS 线程中 div1 的所有属性叫做 div1 的 Property
- 渲染引擎中 div1 对应标签的属性叫做 Attribute
关于两者:
- 大部分时候,同名的 Property 与 Attribute 的值相等,
- 不是标准属性,他俩只会在一开始相等
- Attribute 只支持字符串; Property 支持字符串、布尔等。