提升对前端的认知,不得不了解Web API的DOM和BOM

918 阅读5分钟

了解Web API的DOM和BOM

这是我参与更文挑战的第9天,活动详情查看:更文挑战

在现代的开发中,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 标签等等。引用百度来做一个解释:

DOM的本质

大家可以看到,上面一层一层递进的关系,把整个 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

控制台打印结果如下:

property

从上面的结果中可以看到,通过修改 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'));

控制台打印结果如下:

attribute

从上面的结果中可以看到,通过修改 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性能优化1

按照预想的,呈现出了我们想要的结果,且看着好像也没什么问题。但是呢,这就违反了我们所说的一次性操作 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性能优化2

通过代码我们可以发现,通过创建一个文档片段,来对节点进行一个缓存,等到全部节点操作都完成以后,再统一插入到 DOM 结构中。这种方法,也极大提高了程序中的性能。

5、回顾

最后,我们用几个题目来回顾DOM的知识点。

(1)DOM是哪一种数据结构?

DOM是一种树的数据结构,DOM也常被称为DOM树。

(2)DOM操作的常用API

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

从上图中可以看到,通过 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

相信从上图的演示之后,大家对 location 属性的应用有了有一定的了解。

4、history

history 主要用到 back 和 forward 属性,当网页执行history.back()代码时,会将当前网页向后退一页;当网页执行history.forward()代码时,会将当前网页向前进一页。如以下代码操作:

// history
history.back(); //对网页进行后退
history.forward(); //对网页进行前进

此时浏览器打印如下。

history

从上图中可以看到,通过 history.back() 和 history.forward() 可以让当前浏览页面进行前进或者后退操作。

三、结束语

JS 的基础知识规定了 ECMA 的语法标准,而 Web API 则是网页操作的 API ,是 W3C 的标准。如果要说两者的关系,那自然是 ES 标准是 Web API 的基础。在实际应用开发中,只有两者结合才能真正做到实际应用。所以,不管是 ES 标准还是 Web API 中的 DOM 和 BOM 操作,在实际开发中都是至关重要的内容。

关于 DOM 和 BOM 的操作就讲到这里啦!如有疑问欢迎评论区评论或私信我交流~

  • 关注公众号 星期一研究室 ,不定期分享学习干货,学习路上不迷路~
  • 如果这篇文章对你有帮助,记得点个赞加个关注再走哦~