Skip to content

引言

在现代的开发中,vue 和 react 都是很流行的开发框架,框架虽好用,但是框架的原理还是基于 DOM 操作去实现。如果一个前端工程师只会框架,不会 DOM ,那基本上是很容易被淘汰的。因为框架的存活时间我们谁也说不准,且技术更新迭代也特别快,说不定三五年就会被淘汰了都有可能。所以,扎实的学会 js 的基础原理,不要被框架和一些外部事件所迷惑,对自己会有一个更好的竞争力提升。

本文将讲解 JSWeb APIDOMBOM 操作。

正文

一、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>
<?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>
<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形式
 */
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

从上面的结果中可以看到,通过修改 DOMJS 变量,从而操作 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>
<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形式
 */
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 结构的节点属性,最终得到我们想要的结果。

综上所述, porpertyattribute 这两种操作类型,主要有以下区别:

  • 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>
<!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);
// 新建节点
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);
// 移动节点 -> 把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);
// 获取父元素
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]);
// 移除元素
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 查询
}
// 不缓存 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 查询
}
// 缓存 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>
<!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);
}
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);
/**
 * 频繁操作改为一次性操作
 */
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);
//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
console.log(screen.width); //获取屏幕宽度
console.log(screen.height); //获取屏幕高度

此时浏览器打印如下。

screen

从上图中可以看到,通过 screen.widthscreen.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
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 主要用到 backforward 属性,当网页执行 history.back()代码时,会将当前网页向后退一页;当网页执行 history.forward()代码时,会将当前网页向前进一页。如以下代码操作:

js
// history
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 中的 DOMBOM 操作,在实际开发中都是至关重要的内容。

关于 DOMBOM 的操作就讲到这里啦!如有疑问欢迎勘误~

Released under the MIT License.