携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情
什么是 DOM 和 BOM?
- DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。
这里.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 详解》
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>
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 树上的节点。