DOM 和 BOM

193 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情

什么是 DOM 和 BOM?

  • DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。

image-20220204190238360

这里.click 会在 hi 后执行, 执行后执行 bye. 而 button clicked 会存在 webapi 里, 直到点击才触发回调

DOM 事件也使用回调, 基于 eventloop 但不是异步

DOM 数据结构是一棵树, 本质上是 HTML 文件解析出来一层一层的树

  • BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的方法和接口。BOM的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。

详细资料可以参考: 《DOM, DOCUMENT, BOM, WINDOW 有什么区别?》 《Window 对象》 《DOM 与 BOM 分别是什么,有何关联?》 《JavaScript 学习总结(三)BOM 和 DOM 详解》

image-20220205174649455

image-20220205174727717

8.1 DOM 节点的获取

DOM 节点的获取的API及使用:

 getElementById // 按照 id 查询
 getElementsByTagName // 按照标签名查询
 getElementsByClassName // 按照类名查询
 querySelectorAll // 按照 css 选择器查询
 ​
 // 按照 id 查询
 var imooc = document.getElementById('imooc') // 查询到 id 为 imooc 的元素
 // 按照标签名查询
 var pList = document.getElementsByTagName('p')  // 查询到标签为 p 的集合
 console.log(divList.length)
 console.log(divList[0])
 // 按照类名查询
 var moocList = document.getElementsByClassName('mooc') // 查询到类名为 mooc 的集合
 // 按照 css 选择器查询
 var pList = document.querySelectorAll('.mooc') // 查询到类名为 mooc 的集合

如果有文本, 获取 p 标签时如果直接用 childNodes 就会出现 text,这是可以用过滤器

 console.log(p1.parentNode)
 const div1ChildNodes = div1.childNodes
 console.log(div1ChildNodes[2].nodeType)
 const div1ChildNodesP = Array.prototype.slice.call(div1.childNodes).filter(child=>{
     if(child.nodeType===1){
         return true
     }else{
         return false
     }
 }) 
 console.log('div1ChildNodesP',div1ChildNodesP)

8.1.1. ele.getElementsByClassName和ele.querySelectorAll的区别?

 element.getElementsByClassName 返回一个即时更新(动态的)HTMLCollection
 element.querySelectorAll 返回一个非即时更新(静态的) NodeList
 // 先说什么叫即时更新,(前者是动态的,改变 DOM 结构会同步,后者只会记录调用 api 时的结果,不懂可以看下面的例子)
 <div id="parent">
   <p class="p">1</p>
   <p class="p">2</p>
   <p class="p">3</p>
 </div>
 <script>
 let list1 = parent.getElementsByClassName('p');
 let list2 = parent.querySelectorAll('.p');
 console.log(list1.length1); // 3
 console.log(list2.length1); // 3
 let newP = docuemnt.createElement("p")
 newP.classList.add('p');
 parent.appendChild(newP);
 console.log(list1.length1); // 4 (即时更新)
 console.log(list2.length1); // 3(非即时更新)
 </script>
 // 在说下返回值
 // HTMLCollection 和 NodeList 都是类数组形式
 如下一个 div 可以看成是 HTMLDivElement 的实例,其中 Node 的集合为 NodeList;Element 的集合为 HTMLCollection
 EventTarget - Node - Element - HTMLElement - HTMLDivElement<br>
 EventTarget - Node - Element - SVGElement - SVGPathElement<br>

MDN 上元素 div 继承关系

8.2 DOM 节点的创建

创建一个新节点,并把它添加到指定节点的后面。 已知的 HTML 结构如下:

 <html>
   <head>
     <title>DEMO</title>
   </head>
   <body>
     <div id="container"> 
       <h1 id="title">我是标题</h1>
     </div>   
   </body>
 </html>

要求添加一个有内容的 span 节点到 id 为 title 的节点后面,做法就是:

 // 首先获取父节点
 var container = document.getElementById('container')
 // 创建新节点
 var targetSpan = document.createElement('span')
 // 设置 span 节点的内容
 targetSpan.innerHTML = 'hello world'
 // 把新创建的元素塞进父节点里去
 container.appendChild(targetSpan)

8.3 DOM 节点的删除

删除指定的 DOM 节点, 已知的 HTML 结构如下:

 <html>
   <head>
     <title>DEMO</title>
   </head>
   <body>
     <div id="container"> 
       <h1 id="title">我是标题</h1>
     </div>   
   </body>
 </html>

需要删除 id 为 title 的元素,做法是:

 // 获取目标元素的父元素
 var container = document.getElementById('container')
 // 获取目标元素
 var targetNode = document.getElementById('title')
 // 删除目标元素
 container.removeChild(targetNode)

或者通过子节点数组来完成删除:

 // 获取目标元素的父元素var container = document.getElementById('container')
 // 获取目标元素var targetNode = container.childNodes[1]
 // 删除目标元素container.removeChild(targetNode)

8.4 修改 DOM 元素

修改 DOM 元素这个动作可以分很多维度,比如说移动 DOM 元素的位置,修改 DOM 元素的属性等。

将指定的两个 DOM 元素交换位置, 已知的 HTML 结构如下:

 <html>
   <head>
     <title>DEMO</title>
   </head>
   <body>
     <div id="container"> 
       <h1 id="title">我是标题</h1>
       <p id="content">我是内容</p>
     </div>   
   </body>
 </html>

现在需要调换 title 和 content 的位置,可以考虑 insertBefore 或者 appendChild:

 // 获取父元素
 var container = document.getElementById('container')   
  
 // 获取两个需要被交换的元素
 var title = document.getElementById('title')
 var content = document.getElementById('content')
 // 交换两个元素,把 content 置于 title 前面
 container.insertBefore(content, title)

8.5 DOM property 和 attribute

 const pList = document.querySelectorAll('p')
 const p1 = pList[0]
 //property
 p1.style.width='100px'
 console.log(p1.style.width)
 p1.className='red'
 console.log(p1.className)
 console.log(p1.nodeName)
 console.log(p1.nodeType)
 //attribute
 p1.setAttribute('data-name','imooc')
 console.log(p1.getAttribute('data-name'))
 p1.setAttribute('style','font-size:50px;')
 console.log(p1.getAttribute('style'))

property 是对 dom 元素, js 变量进行修改, 即修改对象属性, 不会体现到 html 结构中

attribute 是对节点属性进行修改, 修改 html 属性, 会改变 html 结构

两者都可能引起 DOM 重新渲染, 一般用 property

8.6 DOM 性能

DOM 操作频繁容易造成卡顿

对 DOM 查询做缓存

频繁操作改成一次性操作

创建一个文档片段, 此时还没有插入到 DOM 结构中

先插入文档片段中

都完成之后, 再统一插入到 DOM 结构中

8.7 DOM 操作——怎样添加、移除、移动、复制、创建和查找节点?

(1)创建新节点

 createDocumentFragment(node);
 createElement(node);
 createTextNode(text);

(2)添加、移除、替换、插入

 appendChild(node)
 removeChild(node)
 replaceChild(new,old)
 insertBefore(new,old)

(3)查找

 getElementById();
 getElementsByName();
 getElementsByTagName();
 getElementsByClassName();
 querySelector();
 querySelectorAll();

(4)属性操作

 getAttribute(key);
 setAttribute(key, value);
 hasAttribute(key);
 removeAttribute(key);

详细资料可以参考: 《DOM 概述》 《原生 JavaScript 的 DOM 操作汇总》 《原生 JS 中 DOM 节点相关 API 合集》

8.8 什么是 Virtual DOM?为什么 Virtual DOM 比原生 DOM 快?

 我对 Virtual DOM 的理解是,
 ​
 首先对我们将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后我们将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
 ​
 当页面的状态发生改变,我们需要对页面的 DOM 的结构进行调整的时候,我们首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
 ​
 最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。
 ​
 我认为 Virtual DOM 这种方法对于我们需要有大量的 DOM 操作的时候,能够很好的提高我们的操作效率,通过在操作前确定需要做的最小修改,尽可能的减少 DOM 操作带来的重流和重绘的影响。其实 Virtual DOM 并不一定比我们真实的操作 DOM 要快,这种方法的目的是为了提高我们开发时的可维护性,在任意的情况下,都能保证一个尽量小的性能消耗去进行操作。

详细资料可以参考: 《Virtual DOM》 《理解 Virtual DOM》 《深度剖析:如何实现一个 Virtual DOM 算法》 《网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?》

8.9 如何比较两个 DOM 树的差异?

 两个树的完全 diff 算法的时间复杂度为 O(n^3) ,但是在前端中,我们很少会跨层级的移动元素,所以我们只需要比较同一层级的元素进行比较,这样就可以将算法的时间复杂度降低为 O(n)。
 ​
 算法首先会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个序号。在深度遍历的时候,每遍历到一个节点,我们就将这个节点和新的树中的节点进行比较,如果有差异,则将这个差异记录到一个对象中。
 ​
 在对列表元素进行对比的时候,由于 TagName 是重复的,所以我们不能使用这个来对比。我们需要给每一个子节点加上一个 key,列表对比的时候使用 key 来进行比较,这样我们才能够复用老的 DOM 树上的节点。