DOM

184 阅读7分钟

概述

我们可以把网页看做成一棵树, JavaScript 是通过 document 来操作这棵树的。

1669545014885.png

1669545050645.png

JavaScript 用window.document操作网页。

  • 在浏览器控制台输入window.document会得到一个:#document 文档,可以拿到根节点,根节点中包含其他节点
  • window.document 可以获取得到网页所有的元素

DOM (Document Object Model)全称是“文档对象模型”。是 JavaScript 操作网页的接口。作用把网页转成一个 JavaScript 对象,从而可以用脚本对其进行操作(例如增删内容)。

节点

DOM 的最小组成单位叫做节点(node)。文档的树形结构(DOM 树),就是由各种不同类型的节点组成。

节点有以下类型:

  • Document:整个文档树的顶层节点
  • DocumentTypedoctype标签(比如<!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

image.png

获取html标签和获取所有标签的区别

html 只是一个根元素,获取其他元素还要展开才有;

获取所有元素标签指获取html、body、head、div等所有元素,一字排开全部展示

image.png

节点的增删改查

创建一个标签节点

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 = '你好'

在页面显示

  1. 自己创建的标签默认处于 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 = 0div1.style.left = 0div1.style.color = 'red'div1.style.fontSize = '100px'div1.style.background = 'white'
  1. 一个元素不能出现在两个地方,例如创建一个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')

例如: image.png 打印出的两种结果不一样 image.png

改事件处理函数

image.png

改内容

  • 改文本内容 (两种)
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;

(一般不用 childNodeschildNodes 包含文本节点排除起来比较麻烦)

  • 查前一个节点
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对象。如下图示: image.png

  • 插入新标签的完整过程
  1. 在div1放入页面之前

我们对div1的所有操作都只在JS线程里

  1. 在div1 放入页面之时

浏览器会发现JS的意图,并通知渲染线程在页面中渲染div1对应的元素

  1. 在div1放入页面之后

我们对 div1 的操作有可能会触发重新渲染,比如改属性, div1.id = 'newid', div1.title = 'new',也可能不会触发。如果对 div1进行多次操作,浏览器有可能会合并成一次操作。

比如下面的例子:

image.png 做一个动画,让 test 宽度从100变成200

先给 test 加一个‘start’类,再加一个‘end’类。发现没有动画效果 因为浏览器发现在短时间内对 classList 进行了两次操作,它会合并成一次。动画效果就没有了

加 test.clienWidth 后阻止了合并,因为在先给 test 加一个‘start’类后,要获取一下 test.clienWidth 。所以浏览器必须先立刻渲染第一次,获取客户端宽度后再渲染第二次。所以虽然 test.clienWidth 没有啥效果,但阻止了合并。

  • 属性同步

我们在JS里修改了 div1 的属性,那渲染线程里的div1属性会自动同步吗?

  1. 标准属性会同步过去
  2. data-* 属性会同步过去
  3. 自定义属性或者非标准属性不会同步过去。(如果想要自定义属性同步,需要用 data- 做前缀)

image.png

  • Property 与 Attribute
    • JS 线程中 div1 的所有属性叫做 div1 的 Property
    • 渲染引擎中 div1 对应标签的属性叫做 Attribute

关于两者:

  1. 大部分时候,同名的 Property 与 Attribute 的值相等,
  2. 不是标准属性,他俩只会在一开始相等
  3. Attribute 只支持字符串; Property 支持字符串、布尔等。