第六章 DOM核心

53 阅读12分钟

WebAPI概述

WebAPI可分为BOM和DOM两大部分:

  • BOM:Brower Object Model,浏览器对象模型
  • DOM:Document Object Model,文档对象模型

BOM用于控制浏览器本身

DOM用于控制HTML页面

DOM的核心理念,就是将HTML或XML文档,用一个个的对象进行表示,每个对象称为DOM对象

DOM对象又称为DOM节点(Node),节点的类型可分为:

  • DocumentType:文档类型节点,在HTML5文档中表示<!DOCTYPE html>

  • Document:文档节点,表示整个文档,它是其它所有DOM节点的祖先节点

    整个HTML文档并不是指html元素,html元素只是HTML文档的一部分

  • Comment:注释节点

  • Element:元素节点

  • Text:文本节点

  • Attribute:属性节点

  • DocumentFragment:文档片段节点

DOM树

DOM树是若干个DOM节点组成的一种树形结构,DOM树固定以Document节点作为根节点

比如下面的HTML文档,对应的DOM树结构为:

<!DOCTYPE html>
<html lang="en">
	<head>
        <meta charset="UTF-8">
        <title>标题</title>
    </head>
    <body>
        <!-- 注释 -->
        <p>lorem</p>
    </body>
</html>

image.png

该DOM树中忽略了许多换行回车这种文本节点,因此这棵DOM树并不完全对应上面的HTML文档

获取DOM节点

window对象中有一个叫做document的属性,该属性是一个对象,这个对象就是Document文档节点

在下面获取节点的操作中,若获取不到,则都返回null

获取元素节点

以前的做法:

  • document.body

    获取body元素节点

  • document.head

    获取head元素节点

  • document.links

    获取页面中所有的a元素节点,得到一个类数组

  • document.anchors

    获取页面中所有的具有name属性的a元素节点,得到一个类数组

  • document.forms

    获取页面中所有的form元素节点,得到一个类数组

现在的做法:

  • document.documentElement

    获取html元素

  • document.getElementById()

    通过id获取对应的第一个元素

  • document.getElementsByTagName()

    通过元素名称获取对应元素的集合,得到一个类数组

  • document.getElementsByClassName()

    通过类名获取对应元素的集合,得到一个类数组

  • document.getElementsByName()

    通过name属性获取对应元素的集合,得到一个类数组

  • document.querySelector()

    通过CSS选择器获取对应的第一个元素

  • document.querySelectorAll()

    通过CSS选择器获取对应元素的集合,得到一个类数组

细节
  1. 在所有得到元素集合的方法中,除了querySelectorAll之外,其它的方法得到的结果都是实时更新的

    // 假设页面中存在10个div
    
    var divs1 = document.getElementsByTagName("div");
    var divs2 = document.querySelectorAll("div");
    // 此时divs1和divs2都保存有页面中10个div的节点信息
    
    // 在divs1和divs2已经得到了匹配的结果后,将页面中的某个div删除
    
    console.log(divs1);			// divs1中保存的信息数量变为9个,那个删除的div节点已经不在
    console.log(divs2);			// divs2中保存的信息数量仍然是10个,那个删除的div节点还在
    
    // 注意:删除div后并没有重新给divs1和divs2赋值
    

    实时性可能会在某些时候带来一些问题:

    <!DOCTYPE html>
    <ul>
        <li>li1</li>
        <li>li2</li>
        <li>li3</li>
        <li>li4</li>
    </ul>
    
    <button id="delAll">del li</button>
    
    <script>
        var list = document.querySelectorAll("li");
        // var list = document.getElementsByTagName("li");	// 没有forEach方法
        delAll.onclick = function () {
            list.forEach(function (item) {
                item.remove();
            });
            // for (var i = 0; i < list.length; i++) {
            //     list[i].remove();
            // }
        }
    </script>
    
  2. getElementById得到元素的效率最高

  3. 添加了id的元素,会自动成为window对象上的属性,并且该属性是实时的

    实时的是指将id对应的元素从页面中删除后,该属性也将从window对象上删除

  4. getElementsByTagName()、getElementsByClassName()、querySelector()、querySelectorAll()这四个方法除了在document文档节点上使用外,也可以在元素节点中使用,在元素节点中使用时可以减小查找范围,提升查找速度

    如:变量ul保存有某个ul元素节点,则可以使用ul.querySelector("li")查找ul内部的第一个li元素

  5. 使用dom.tagName获取dom元素节点的标签名,得到的是全大写的字符串

根据节点间关系获取其他节点

这里所指的节点可以是任何类型的节点

  • node.parentNode

    获取当前节点的父节点

  • node.parentElement

    获取当前节点的父元素节点

  • node.previousSibling

    获取当前节点的上一个兄弟结点

  • node.previousElementSibling

    获取当前节点的上一个兄弟元素结点

  • node.nextSibling

    获取当前节点的下一个兄弟结点

  • node.nextElementSibling

    获取当前节点的下一个兄弟元素结点

  • node.childNodes

    获取当前节点的所有子节点,得到一个类数组

  • node.children

    获取当前节点的所有子元素节点,得到一个类数组

  • node.firstChild

    获取当前节点的第一个子节点

  • node.firstElementChild

    获取当前节点的第一个子元素节点

  • node.lastChild

    获取当前节点的最后一个子节点

  • node.lastElementChild

    获取当前节点的最后一个子元素节点

  • elementNode.attributes

    获取当前元素节点的所有属性节点,得到一个类数组

通用的节点属性

通用的节点属性是指任意类型节点都具有的属性

  • node.nodeName

    获取节点名称

    元素节点的nodeName为元素标签名的全大写形式

  • node.nodeValue

    获取节点的值

  • node.nodeType

    获取节点的类型,返回一个数字

    节点类型对应数值
    元素节点1
    属性节点2
    文本节点3
    注释节点8
    文档节点9
    文档类型节点10
    文档片段节点11

DOM元素节点操作

获取和设置元素的属性

元素的属性可以分为可识别属性和自定义属性两种:

  • 可识别属性:元素自带的属性,如a元素的href,img的src,input的value等
  • 自定义属性:HTML 元素标准中未定义的属性

获取元素的可识别属性:

  • 使用dom.xxx

    推荐使用

  • 使用dom.getAttribute("xxx")

    慎用

设置元素的可识别属性:

  • 使用dom.xxx = "xxx"

    推荐使用

  • 使用dom.setAttribute("xxx", "xxx")

    慎用

获取元素的自定义属性:使用dom.getAttribute("xxx")

设置元素的自定义属性:使用dom.setAttribute("xxx", "xxx")

细节

可识别属性:

  • 可识别属性始终存在,而不管元素代码上是否书写了该属性

  • 可识别属性中的布尔属性,使用dom.的方式获取,得到的是布尔值

  • 可识别属性中的路径类属性,使用dom.的方式获取,得到的是绝对路径

  • getAttribute方法获取元素的可识别属性,是基于元素代码的,代码上是什么,得到的就是什么,代码上没有,则获取不到

    dom.设置的可识别属性,有时并不会导致元素代码位置的属性的值变化,因此getAttribute得到的可能与实际元素的展示效果不一致,因此慎用这种方法获取可识别属性

  • setAttribute方法设置元素的可识别属性,设置的值会使用String(设置的值)的方式转换为字符串后,再作为属性值加入到元素源代码位置处

    setAttribute设置可识别属性,属性的效果有时可能并不会生效,因此慎用

  • 某些可识别属性,它们的属性名与某些关键字重名,因此这部分属性在dom元素节点中被改为了另外的名称

    如元素的class属性,在获取时应该使用dom.className进行获取

    label元素的for属性,在获取时应该使用label.htmlFor进行获取

  • 某些元素(大部分为表单元素),可以获取到它不具有的可识别属性

    如:value属性并不是select元素的可识别属性,但依然可以通过select得到value

    <!DOCTYPE html>
    <select id="select">
        <option value="beiing">北京</option>
        <option value="shanghai">上海</option>
        <option value="shenzhen" selected>深圳</option>
    </select>
    
    <script>
    	console.log(select.value);		// shenzhen
    </script>
    

    再比如:textarea元素的可识别属性不包含value,元素的内容是添加在元素的起始标记和结束标记之间的,但仍可以使用textarea.value来获取到元素中的内容

    <!DOCTYPE html>
    <textarea id="textarea">这是一段内容</textarea>
    
    <script>
    	console.log(textarea.value);		// "这是一段内容"
    </script>
    

自定义属性:

  • 自定义属性可以通过removeAttribute("xxx")进行删除

  • HTML5建议在自定义元素的属性时,给属性名加上data-前缀

    拥有data-前缀的自定义属性,可以使用dom.dataset.xxx的方式获取到该自定义属性,也可以使用dom.dataset.xxx = xxx的方式设置该自定义属性,还可以使用delete dom.dateset.xxx来删除该自定义属性

获取和设置元素的内容

  • dom.innerHTML

    获取和设置元素内部的html片段

  • dom.innerText

    获取和设置元素内部的文本内容

    获取时忽略dom内部的元素的标记部分,设置时则会完全覆盖内部的内容

  • dom.textContent

    获取和设置元素内部的文本内容

    获取时忽略dom内部的元素的标记部分,设置时则会完全覆盖内部的内容

    textContent也可以作为文本节点的属性存在

textContent与innerText的区别:

  • innerText获取和设置的是元素最终在页面上展示出来的文本

    textContent获取和设置的是元素在代码中所含有的内容

    <!DOCTYPE html>
    <div id="div">
        <style>
            span{
                color: red;
            }
        </style>
        <span>
            span
            	content
        </span>
        <p style="display: none">p content</p>
    </div>
    
    <script>
    	console.log(div.innerText);
        console.log(div.textContent);
    </script>
    

    image.png

元素结构重构

  • dom1.appendChild(dom2)

    将dom2作为dom1的最后一个子元素加入进去

    若dom2已经是dom1的子元素,则dom2将成为dom1的最后一个子元素

    注意:该方法并不会克隆dom2,它是将dom2从一个地方移动到了另一个地方

  • dom.append()

    和appendChild的区别在于append可以一次传入多个参数

  • dom1.insertBefore(dom2, dom3)

    在dom1的子元素dom3的前面插入dom2

  • dom1.replaceChild(dom2, dom3)

    将dom1的子元素dom3替换为dom2

上面的方法对非元素节点也适用

更改元素结构会导致重排,效率较低

创建和删除元素

  • document.createElement("元素名")

    创建一个元素对象

    创建后dom对象仅保存在内存中,还未添加至页面中

  • document.createTextNode("文本")

  • document.createComment("注释")

  • document.createDocumentFragment()

    创建文档片段节点

    文档片段用于保存多个其它结点

    文档片段可以理解为一种容器,用于容纳其他节点,它自身并不会表现出什么实际的展示效果

    假设要使用js将100个li插入到ul中,那么可以使用循环,循环100次,每次appendChild一个li,但这样做并不好,因为每循环一次都会导致页面结构被修改一次

    此时就可以利用文档片段,先将这100个li加入到文档片段中,最后再一次性将文档片段中的所有li加入到ul中

    var frag = document.createDocumentFragment();
    for(var i = 0; i < 100; i++){
    	var li = document.createElement("li");
        frag.appendChild(li);
    }
    ul.appendChild(frag);
    
  • dom.cloneNode()

    不传参或传入false:浅克隆,克隆dom本身和dom上的属性,但不包括dom内部的节点(文本节点、元素节点等)

    传true:深克隆,克隆dom本身和dom上的属性,以及dom内部的所有节点(文本节点、元素节点等)

  • dom1.removeChild(dom2)

    删除dom1的子元素dom2(包括dom2内部的节点),会返回删除的元素

  • dom.remove()

    删除dom自身(包括dom内部的元素)

上面的方法对非元素节点也适用

DOM元素样式

类名控制

  • dom.className

    通过设置dom元素的className属性,来控制元素拥有的类名

  • dom.classList

    dom.classList.add("类名"):给元素添加类名

    dom.classList.remove("类名"):删除元素的某个类名

    dom.classList.toggle("类名"):切换类名,已存在则删除,不存在则添加

    dom.classList.contains("类名"):判断类名是否存在

获取样式

  • dom.style

    得到元素的行内样式对象

    行内样式对象中包含所有css属性,但所有css属性的属性值都是根据元素的style属性的内容确定的,在元素的style属性中书写了的css样式,值与行内中声明的值一致,否则值为空串

    行内样式并不是最终计算出的样式,通过行内样式对象得到的样式值可能会与实际的元素显示效果不一致

    <!DOCTYPE html>
    <style>
        div{
            width: 100px;
            height: 100px;
            background-color: red !important;
        }
    </style>
    
    <div id="div" style="background-color: green"></div>
    
    <script>
    	console.log(div.style.backgroundColor);		// "green"
    </script>
    
  • window.getComputedStyle(dom)

    获取dom元素最终计算出的样式

    最终计算出的样式就是经过【属性值的计算过程】步骤后的样式,包含所有css样式

    最终计算出的样式的值,只要能转换为具体单位,就会转换为具体单位(如em会转换为px、颜色的预设值会转换为rgb、以及inherit属性值会转换为父元素的属性值等),因此该方法得到的属性值大部分是绝对值

    <!DOCTYPE html>
    <div id="div" style="background-color: red; width: 100px; height: 100px; color: red;">
        <span id="span">text</span>
    </div>
    
    <script>
        var style1 = getComputedStyle(div);
        var style2 = getComputedStyle(span);
        console.log(div.style.backgroundColor);		// "red"
        console.log(span.style.color);				// ""
        console.log(style1.backgroundColor);		// "rgb(255, 0, 0)"
    	console.log(style2.color);					// "rgb(255, 0, 0)"
    </script>
    

    该函数可以传入第二个参数,用于获取dom的伪元素的样式,传入"before"则获取before,传入"after"则获取after

    该方法得到的样式对象是实时的,一经获取,即使元素的样式发生改变,对象中的对应样式值也会跟着改变

    <!DOCTYPE html>
    <style>
        div{
            width: 100px;
            height: 100px;
            background-color: pink;
            position: fixed;
            left: 0;
            top: 0;
        }
    </style>
    
    <div id="div"></div>
    
    <script>
        var style = getComputedStyle(div);
    	setInterval(function(){
            console.log(parseInt(style.left));
            div.style.left = parseInt(style.left)+ 10/3 + "px";
        }, 10);
    </script>
    

注意:使用dom.style.获取css属性值的方式是直接将元素的style属性中对应css样式的样式值返回(没设置会返回空串),因此获取它的值不会导致浏览器重新计算dom的尺寸和位置,也就不会导致页面重排;而采用getComputedStyle(dom).xxx则会导致页面立即重排,因为它的结果需要浏览器将元素的位置和尺寸进行计算后才能确定

<!DOCTYPE html>
<style>
    div {
        width: 100px;
        height: 100px;
        background-color: lightblue;
        position: relative;
    }
</style>

<div id="div"></div>

<script>
    div.onclick = function () {
        div.style.left = "0px";
        div.style.transition = "1s";
        // 此处需要强制浏览器reflow,以防left被覆盖导致触发不了过渡效果
        // getComputedStyle(div).left;		// 将导致reflow
        // div.style.left;					// 不会导致reflow
        div.style.left = "100px";
    }
</script>

dom.getBoundingClientRect().xxx也会导致重排

设置样式

  • dom.style.xxx = "xxx"

    设置元素的行内样式