Skip to content

引言 ​

在现代的开发中,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
<?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 代码:

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 代码:

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 代码:

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 代码:

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 代码。

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 ,代码如下。

js
// 新建节点
const newP = document.createElement('p');
newP.innerHTML = 'this is newP';
// 插入节点
div1.appendChild(newP);

此时浏览器执行状态如下。

新增子节点

大家可以看到, div1 成功新增了一个节点,这就是关于新增节点操作的具体流程。

那如果在此基础上,要移动一个节点呢?具体操作如下。

js
// 移动节点 -> 把p1移动到div2中来
const p1 = document.getElementById('p1');
div2.appendChild(p1);

此时浏览器执行状态如下。

移动节点

(2)获取子元素列表,获取父元素 ​

在上面的基础上,我们来看看获取父元素和子元素列表的流程。

js
// 获取父元素
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 节点进行移除。

js
// 移除元素
div1.removeChild(div1ChildNodesP[0]);

此时浏览器执行状态如下。

移除节点

以上就是对 DOM 结构的新增、移动、获取子父元素以及删除子元素的一个操作,相信大家对 DOM 结构的增删改有了一个新的了解。接下来我们讲解 DOM 性能。

4、DOM 性能 ​

为什么要对 DOM 做性能优化呢,原因在于 DOM 操作是非常“昂贵”的,每一次操作都很有可能引发浏览器的重排和重绘,因此要避免频繁的 DOM 操作。那如何做到避免频繁的 DOM 操作,给 DOM 进行性能优化呢?主要有以下两个方面给 DOM 操作进行性能优化:

  • 对 DOM 查询做缓存
  • 将频繁操作改为一次性操作

接下来将对这两种方法进行讲解。

(1)对 DOM 查询做缓存 ​

js
// 不缓存 DOM 查询结果
for (let i = 0; i < document.getElementsByTagName('p').length; i++) {
  // 每次循环,都会计算length,频繁进行 DOM 查询
}
js
// 缓存 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 代码

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 代码

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 ,一直在频繁操作中修改。因此,我们可以通过以下方法来对性能做一个优化:

js
/**
 * 频繁操作改为一次性操作
 */
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 表示获取浏览器的用户代理字符串。如以下代码操作:

js
//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 表示获取当前屏幕的高度。如以下代码操作:

js
// screen
console.log(screen.width); //获取屏幕宽度
console.log(screen.height); //获取屏幕高度

此时浏览器打印如下。

screen

从上图中可以看到,通过 screen.width 和 screen.height 可以获取到当前屏幕的宽度和高度。

3、location ​

location 主要用到 href/protocol/pathname/search/hash 属性,具体含义如下代码所示。

js
// 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()代码时,会将当前网页向前进一页。如以下代码操作:

js
// 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 的操作就讲到这里啦!如有疑问欢迎勘误~

Released under the MIT License.