引言
在现代的开发中,vue和react都是很流行的开发框架,框架虽好用,但是框架的原理还是基于 DOM
操作去实现。如果一个前端工程师只会框架,不会 DOM
,那基本上是很容易被淘汰的。因为框架的存活时间我们谁也说不准,且技术更新迭代也特别快,说不定三五年就会被淘汰了都有可能。所以,扎实的学会 js
的基础原理,不要被框架和一些外部事件所迷惑,对自己会有一个更好的竞争力提升。
本文将讲解 JS
中 Web API
的 DOM
和 BOM
操作。
正文
一、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
操作,在实际开发中都是至关重要的内容。
关于 DOM
和 BOM
的操作就讲到这里啦!如有疑问欢迎评论区评论或私信我交流~
关注公众号 星期一研究室 ,不定期分享学习干货
如果这篇文章对你有用,记得点个赞加个关注哦~