DOM基础相关

254 阅读9分钟

背景

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 操作的基础。

  1. 建一个标签节点
  • document.createElement('标签名')
  • let div1 = document.createElement('div')
  • 你创建的标签默认处于JS线程中,内存中
  1. 创建一个文本节点
  • text1 = document.createTextNode("你好")
  1. 标签里面插入文本
  • div1. appendChild(text1) 必须加文本节点的名字,不可以加文本的内容
  • div1.innerText='你好' (Node原型提供)
  • div1.textContent ='你好'(Element原型提供)
  1. 把新创建的标签插入页面中
  • 必须把我们刚刚创建在js线程中的标签插到已在页面里的标签里面,它才会生效
  • head或者body,document.body.appendChild(div1)
  • 或者已在页面中的元素.appendChild(div1)
  1. appendChild() 方法
  • appendChild() 方法可向节点的子节点列表的末尾添加新的子节点。
  • node.appendChild(node)
  • 相同的子节点不可以出现在两个地方:如果同时把一个子节点又插入节点1,又插入节点2 ,那该子节点最终会在节点2中
  • 除非你把这个子节点复制一份div2 = div1.cloneNode(),如果传递给它的参数是 true,它还将递归复制当前节点的所有子孙节点。否则,它只复制当前节点。

  1. 旧方法节点提供的: parentNode.removeChild(childNode)
  • 找到要删除的节点的父节点,用父节点的removeChild删除
  1. 新方法: 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)
  • 直接这样就可以了,直接从原来的地方消失

查节点

  1. 查自己

node

  1. 查爸爸
  • node.parentNode
  • node.parentElement
  1. 查爷爷

node.parentNode.parentNode

  1. 查子代
    (1)所有子代
  • node.childNodes(会把你不想要的空格也当成一个文本子节点)(节点提供的)
  • node.children(这个不会,只是元素节点,优先使用)(元素提供的)
  • 当子代变化时,两者也会实时变化
  • document.querySelectorAll('.red')[0]不会实时更新

(2)一个子代

  • node.children[0]
  • 查看老大node.firstChild
  • 查看老幺node.lastChild
  1. 查兄弟姐妹 (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(元素兄弟)
  1. 查看一个节点里所有的元素(儿子、孙子、曾孙子,,,)
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不仅可以使用字符串读写,它本身还是一个对象,可以直接读写个别属性。