一、DOM操作
DOM操作,即Document Object Model文档对象模型。下面通过几个知识点来分析DOM
的本质、节点操作、结构操作以及 DOM
的性能。
1、DOM的本质
DOM的本质就是一棵 树 ,是树结构。
我们从早起xml说起,xml是一种可扩展性的标准语言,早期基本是用xml来对DOM进行编写。具体形式如下:
<?xml version = "1.0" encoding = "UTF-8"?> <note> <to>Tony</to> <from>Abby</from> <heading>London</heading> <body>Have a nice day!</body> <other> <a></a> <b></b> </other> </note> 复制代码
我们可以看到,一层一层的下来,层层递进,很像一棵树。
回到现在,我们基本上用的是 html
来进行编写。 html
也是一种特定的 xml
,只不过它是有自己的一套规范,比如说我们常见的 p
标签, span
标签, li
标签等等。引用百度来做一个解释:
大家可以看到,上面一层一层递进的关系,把整个 html
渲染出来形成我们看到的页面,这一层层递进的关系,其实就是 DOM
树,所以也可以说, DOM
是从 html
文件解析出来的一棵树。
2、DOM节点操作
DOM节点主要有两种操作,一种是property操作,另一种是attribute操作。下面让我们来看看这两种操作。
(1)property形式
html代码:
<div id="div1" class="container"> <p>文段 1</p> <p>文段 2</p> <p>文段 3</p> </div> <div id="div2"> <img src="https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2144980717,2336175712&fm=58" alt=""> </div> 复制代码
JS代码:
/** * property形式 */ const pList = document.querySelectorAll('p'); const p1 = pList[0]; p1.style.width = '100px'; //修改样式 console.log(p1.style.width); //获取样式 p1.className = 'red'; //修改class名称 console.log(p1.className); //获取class名称 // 获取nodeName和nodeType console.log(p1.nodeName); //打印节点名称,p console.log(p1.nodeType); //打印节点类型,普通的DOM节点元素为1,文本类型是3 复制代码
控制台打印结果如下:
从上面的结果中可以看到,通过修改 DOM
的 JS
变量,从而操作 DOM
,最终得到我们想要的结果。
(2)attribute形式
html代码:
<div id="div1" class="container"> <p>文段 1</p> <p>文段 2</p> <p>文段 3</p> </div> <div id="div2"> <img src="https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2144980717,2336175712&fm=58" alt=""> </div> 复制代码
JS代码:
/** * attribute形式 */ const pList = document.querySelectorAll('p'); const p1 = pList[0]; p1.setAttribute('data-name', 'immoc'); console.log(p1.getAttribute('data-name')); p1.setAttribute('style', 'font-size:50px;'); console.log(p1.getAttribute('style')); 复制代码
控制台打印结果如下:
从上面的结果中可以看到,通过修改 DOM
结构的节点属性,最终得到我们想要的结果。
综上所述, porperty
和 attribute
这两种操作类型,主要有以下区别:
- property:修改对象属性,不会体现到
html
结构中; - attribute:修改
html
属性,会改变html
结构,即标签结构; - 两者都有可能引起DOM重新渲染,但
attribute
引起DOM
重新渲染的可能性更大,因为它会改动html
的结构。所以在实际开发中,可以选择的话尽量渲染property
去操作DOM
。
3、DOM结构操作
通过上面的了解,我们都明白了DOM是一种树结构。那既然是树结构,就应该可以对节点进行增删改的操作。
因此,DOM结构操作主要有以下三种类型:
- 新增/插入节点;
- 获取子元素列表,获取父元素;
- 删除子元素。
接下来对这三种类型进行讲解。
(1)新增/插入节点
先附上html代码。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="div1" class="container"> <p id = "p1">文段 1</p> <p>文段 2</p> <p>文段 3</p> </div> <div id="div2"> <img src="https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2144980717,2336175712&fm=58" alt=""> </div> <script src="./你的文件路径.js"></script> </body> </html> 复制代码
此时执行状态如下。
如果此时要给 div1
新增一个子节点 p
,代码如下。
// 新建节点 const newP = document.createElement('p'); newP.innerHTML = 'this is newP'; // 插入节点 div1.appendChild(newP); 复制代码
此时浏览器执行状态如下。
大家可以看到, div1
成功新增了一个节点,这就是关于新增节点操作的具体流程。
那如果在此基础上,要移动一个节点呢?具体操作如下。
// 移动节点 -> 把p1移动到div2中来 const p1 = document.getElementById('p1'); div2.appendChild(p1); 复制代码
此时浏览器执行状态如下。
(2)获取子元素列表,获取父元素
在上面的基础上,我们来看看获取父元素和子元素列表的流程。
// 获取父元素 console.log(p1.parentNode); // 获取子元素列表 const div1ChildNodes = div1.childNodes; console.log(div1ChildNodes); //此处获取的包含p标签以及p标签下面的文本,因此需要一层过滤 const div1ChildNodesP = Array.prototype.slice.call(div1.childNodes).filter(child => { if(child.nodeType === 1){ return true; }else{ return false; } }); console.log('div1ChildNodesP', div1ChildNodesP); 复制代码
此时浏览器执行状态如下。
(3)删除子元素
在上面操作的基础上,我们现在对 div1
中的第一个 p
节点进行移除。
// 移除元素 div1.removeChild(div1ChildNodesP[0]); 复制代码
此时浏览器执行状态如下。
以上就是对DOM结构的新增、移动、获取子父元素以及删除子元素的一个操作,相信大家对DOM结构的增删改有了一个新的了解。接下来我们讲解DOM性能。
4、DOM性能
为什么要对DOM做性能优化呢,原因在于 DOM
操作是非常“昂贵”的,每一次操作都很有可能引发浏览器的重排和重绘,因此要避免频繁的 DOM
操作。那如何做到避免频繁的 DOM
操作,给 DOM
进行性能优化呢?主要有以下两个方面给 DOM
操作进行性能优化:
- 对
DOM
查询做缓存 - 将频繁操作改为一次性操作
接下来将对这两种方法进行讲解。
(1)对DOM查询做缓存
// 不缓存 DOM 查询结果 for(let i = 0; i < document.getElementsByTagName('p').length; i++){ // 每次循环,都会计算length,频繁进行 DOM 查询 } 复制代码
// 缓存 DOM 查询结果 const pList = document.getElementsByTagName('p'); const length = pList.length; for(let i = 0; i < length; i++){ // 缓存length,只进行一次 DOM 查询 } 复制代码
分析以上两段代码,在第一段当中,每次循环的时候,都会计算 length
,频繁的对 DOM
进行查询,如此频繁的操作,可想对程序都不是不太好的。
因此,通过优化,我们写出第二段代码。在第二段代码中,把 length
放在外部进行缓存,等到每次循环的时候,只需要进行一次 DOM
查询,而不用像第一段一样频繁操作,极大提高了性能。
(2)将频繁操作改为一次性操作
我们来看一个例子。比如说,我们先在要通过操作 DOM
来一次性插入10个列表。
在正常情况下我们想象的操作如下:
html代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="div1" class="container"> <p id = "p1">文段 1</p> <p>文段 2</p> <p>文段 3</p> </div> <div id="div2"> <img src="https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2144980717,2336175712&fm=58" alt=""> </div> <ul id="list"> </ul> <script src="./你的路径.js"></script> </body> </html> 复制代码
js代码
const list = document.getElementById('list'); for(let i = 0; i < 10; i++){ const li = document.createElement('li'); li.innerHTML = `List item ${i}`; list.appendChild(li); } 复制代码
此时浏览器执行状态如下。
按照预想的,呈现出了我们想要的结果,且看着好像也没什么问题。但是呢,这就违反了我们所说的一次性操作 DOM
的原则,因为它在频繁的操作 DOM
,一直在频繁操作中修改。因此,我们可以通过以下方法来对性能做一个优化:
/** * 频繁操作改为一次性操作 */ const list = document.getElementById('list'); // 创建一个文档片段,此时还没有插入到 DOM 结构中 const frag = document.createDocumentFragment(); for(let i = 0; i < 15; i++){ const li = document.createElement('li'); li.innerHTML = `List item ${i}`; //先插入文档片段 frag.appendChild(li); } // 都完成之后,再统一插入到 DOM 结构中 list.appendChild(frag); 复制代码
执行后,浏览器也同样效果呈现出来了。
通过代码我们可以发现,通过创建一个文档片段,来对节点进行一个缓存,等到全部节点操作都完成以后,再统一插入到 DOM
结构中。这种方法,也极大提高了程序中的性能。
5、回顾
最后,我们用几个题目来回顾DOM的知识点。
(1)DOM是哪一种数据结构?
DOM是一种树的数据结构,DOM也常被称为DOM树。
(2)DOM操作的常用API
(3)attribute和property的区别
两者主要有以下区别:
- property:修改对象属性,不会体现到
html
结构中; - attribute:修改
html
属性,会改变html
结构,即标签结构; - 两者都有可能引起DOM重新渲染,但
attribute
引起DOM
重新渲染的可能性更大,因为它会改动html
的结构。所以在实际开发中,可以选择的话尽量渲染property
去操作DOM
。
(4)一次性插入多个DOM节点,需考虑性能,怎么操作?
可以通过创建一个Fragment的文档片段,来对节点进行一个缓存,等到 全部节点操作 都完成以后,再统一插入到 DOM
结构中,从频繁执行改为一次执行。
二、BOM操作
BOM,即Brouse Object Model浏览器对象模型。下面通过几个知识点来了解浏览器的BOM操作。
1、navigator
navigator
主要用到 userAgent
属性, navigator.userAgent
表示获取浏览器的用户代理字符串。如以下代码操作:
//navigator const ua = navigator.userAgent; console.log(ua); const isChrome = ua.indexOf('Chrome'); console.log(isChrome); 复制代码
此时浏览器打印如下。
从上图中可以看到,通过 userAgent
可以获取到当前所使用浏览器的内核信息。
2、screen
screen
主要用到width和height属性,screen.width表示获取当前屏幕的宽度,sceen.height表示获取当前屏幕的高度。如以下代码操作:
// screen console.log(screen.width); //获取屏幕宽度 console.log(screen.height); //获取屏幕高度 复制代码
此时浏览器打印如下。
从上图中可以看到,通过 screen.width
和 screen.height
可以获取到当前屏幕的宽度和高度。
3、location
location
主要用到 href/protocol/pathname/search/hash 属性,具体含义如下代码所示。
// location console.log(location.href); //获取整个网址 console.log(location.protocol); //获取网址协议 console.log(location.pathname); //获取网址域名 console.log(location.search); //获取网址中的一些参数 console.log(location.hash); //获取哈希值,即#号后面的值 复制代码
此时浏览器打印如下。
相信从上图的演示之后,大家对 location
属性的应用有了有一定的了解。
4、history
history
主要用到 back
和 forward
属性,当网页执行history.back()代码时,会将当前网页向后退一页;当网页执行history.forward()代码时,会将当前网页向前进一页。如以下代码操作:
// history history.back(); //对网页进行后退 history.forward(); //对网页进行前进 复制代码
此时浏览器打印如下。
从上图中可以看到,通过 history.back()
和 history.forward()
可以让当前浏览页面进行前进或者后退操作。
三、结束语
JS
的基础知识规定了 ECMA
的语法标准,而 Web API
则是网页操作的 API
,是 W3C
的标准。如果要说两者的关系,那自然是 ES
标准是 Web API
的基础。在实际应用开发中,只有两者结合才能真正做到实际应用。所以,不管是 ES
标准还是 Web API
中的 DOM
和 BOM
操作,在实际开发中都是至关重要的内容。