DOM 和 JavaScript 的关系
JavaScript 通过 DOM 进入和控制文档,而 DOM 利用 Javascript 提供的核心类型和语法实现更多的功能。
一个完整的 JavaScript 实现应该由核心(ECMAScript)、DOM 和 BOM 组成。
Web 浏览器作为 ECMAScript 实现的宿主环境之一,不仅提供了基本的 ECMAScript 实现,同时也提供了核心语言之外的平台特定对象和功能(如浏览器 API、 window对象)。
- Window 对象
document
对象是网页内容的主要主要接入点。通过使用它,我们可以创建和改变页面中的任何内容。- BOM(Browser Object Model)浏览器宿主环境提供的另一个对象,包括对象
navigator
、location
,函数alert/confirm/prompt
DOM 对象的数据类型
- Document
- Node
- Element
- NodeList
- Attribute
- NamedNodeMap
DOM 节点层次
- Node 类型
- Document 类型
- Element 类型
- Text 类型
- Comment 类型
- CDATSection 类型
- DocumentType 类型
- DocumentFragment 类型
- Attr 类型
Document
表示任何在浏览器中载入的网页,并作为网页内容的入口,也就是 DOM 树。它向网页文档本身提供了全局操作功能,能解决如何获取页面的 URL
,如何在文档中创建一个新元素这样的问题。
Node
位于文档内的每个对象都是某种节点,在 HTML 文档中,一个对象可以是元素节点、文本节点或者是元素节点。
Element
HTML DOM API 的 HTMLElement
接口增强元素对象的能力。
NodeList
一个 nodeList 是一格元素列表
DOM 树
HTML 文档是由标签嵌套而成的树形结构,因此 DOM 可以将任何 HTML 描述成一个由多层节点构成的树形结构。根据文档对象模型(DOM),每个 HTML 标签都是一个对象。
节点类型有 12 种,但通常使用最多的只有以下四种节点:
document
- 文档的根节点- 元素节点 - HTML 标签元素
- 文本节点 - 包含文本
- 注释 - 不会显示,可以读取
遍历 DOM
我们在使用 DOM 操作文档元素之前,需要先获取对应的 DOM 对象,把这个对象赋予一个变量,从而方便后续操作。
所有的 DOM 操作都是从 document
对象开始,这是 DOM 的主要入口,利用它可以进入任何节点。
documentElement 和 body
顶部的树节点可以通过 document
属性来使用
<html>
= document.documentElement
<body>
= document.body
<head>
= document.head
子元素
childNodes
firstChild
lastChild
firstChild 和 lastfirst 属性是访问第一个和最后一个子元素的快捷方式。
childNodes
是一个具有 length
属性,类似数组的可迭代对象,可使用 for .. of
语法或者 forEach(..)
方法来迭代它。
// for..of 语法
for (let node of document.body.childNodes) {
console.log(node);
}
// forEach(..) 方法document.body.childNodes.forEach(el => console.log(el));
注意: DOM 集合是只读的,修改节点信息有其他的方法;DOM 集合是实时变化的。
兄弟节点和父节点
- 兄弟节点
nextSibling
、previousSibling
- 父节点
parentNode
在元素中遍历
上面介绍的 childNodes
、firstChild
、lastChild
、nextSibling
、previousSibling
属性遍历的是所有的节点,包括文本节点、元素节点和注释节点。
而对于很多任务来说,我们只需要操作元素节点,使用下面的属性可以制作元素节点中导航。
children
- 元素节点的子节点firstElementChild
、lastElementChild
- 第一个和最后一个元素节点previousElementSibling
、nextElementSibling
- 兄弟元素parentElement
- 父元素
parentNode 和 parentElement 都获取元素的父节点,但有一个例外:
document.documentElement.parentNode; // document
documentdocument.documentElement.parentElement // null
不同结果的原因:parentNode
获取的是任何类型的父节点,parentElement
获取的是父元素节点,而 document
形式上作为
getElement* 和 querySelector*
document.getElementById(id)
document.getElementsByTagName(tag)
document.getElementsByClassName(className)
document.getElementsByName(name)
elem.querySelector(selector)
elem.querySelectorAll(selector)
matches 和 closest
matches
elem.matches(css)
检查 elem 是否匹配给定的 CSS 选择器,返回 true 或者 false。
<a href="example.com/txt">example.com/txt</a>
<a href="byod.io">byod.io</a>
<script>
const lists = document.querySelectorAll('a');
for (let list of lists) {
if (list.matches('a[href$="txt"]')) {
console.log(list.textContent)
// 'example.com/txt'
}
}
</script>
elem.mathes(css)
常用于事件委托,判断 e.target
是否与给定的 CSS 选择器匹配
<div class="btn-dec">
<button class="search__btn">
<svg class="search__icon">
<use href="img/icons.svg#icon-magnifying-glass"></use>
</svg>
<span>Search</span>
</button>
<button class="sumbit__btn">
<svg class="sumbit__icon">
<use href="img/icons.svg#icon-magnifying"></use>
</svg>
<span>Search</span>
</button>
</div>
<script>document.querySelector('.btn').addEventListener('click', e => {
if (e.target.matches('.search__btn, .search__btn *')) {
// ...code
} else if (e.target.matches('.sumbit__btn, .sumbit__btn *')) {
// ...code
}
});
</script>
closest
elem.closest(css)
会查找与 CSS 选择器匹配的最接近的祖先,包括 elem 自身。
<div class="container">
<ul class="shopping">
<li class="list">list 1</li>
<li class="list">list 2</li>
</ul>
</div>
<script> const li = document.querySelector('.list'); console.log(li.closest('.shopping')); // <ul></script>
elem.closest(css)
也常用于事件委托,判断 e.target
是否与给定的 CSS 选择器匹配
Node
不同的节点或许有不同的属性。
DOM 节点是正常的 JavaScript 对象,它们使用基于原型继承属性和方法。
在浏览器控制台使用 console.dir(elem)
可得到 DOM 对象。
console.log(tree)
展示元素的 DOM 树
属性
DOM 节点是正常的 JavaScript 对象。其中有很多内建的属性和方法,我们可以创建自己的属性和方法,也可以修改原型链上的方法。
- 属性值可以是任何类型
- 属性大小写敏感
内建属性
nodeType
nodeName
innerHTML
outerHTML
nodeValue/data
textContent
返回带格式的文本innerText
返回不带格式的文本
自建属性和方法
document.body.mydata = {
name: 'yo'
title: 'Imperator'
}
document.body.mydata.title // Imperator
document.body.sayTagName = function () {
console.log(this.tagName);
}
document.body.sayTagName(); // BODY
HTML attributes
在 HTML 中,标签可能存在 attributes,当浏览器解析 HTML 为标签创建 DOM 对象时,它会辨别标准 attributes 并为他们创建 DOM 属性。
获取 attributes 值
elem.hasAttribute(name)
elem.getAttribute(name)
elem.setAttribute(name, value)
elem.removeAttribute(name)
读取所有的 attributes
elem.attributes
小结
- Attrubutes 写在 HTML 中
- Properties 写在 DOM 对象中
对于大部分场景,使用 DOM 属性是更合适的,只有当 DOM 属性不适合当前场景时,再参考 HTML Attributes。比如:
- 当需要使用非标准 attribute。
data-
attribute 需要使用dataset
- 当需要读取写在 HTML 中的值,并且在 DOM 属性中是不同类型的这个值,比如,DOM 属性
href
返回绝对的 URL 路径,HTML attribute 只返回相对的路径。
<a id="a" href="#hello">link</a>
<script>
// attribute
console.log(a.getAttribute('href')); // #hello
// property
console.log(a.href ); // full URL in the form http://site.com/page#hello
</script>
<div id="div" style="color:red;font-size:120%">Hello</div>
<script>
// string
console.log(div.getAttribute('style')); // color:red;font-size:120%
// object
console.log(div.style); // [object CSSStyleDeclaration]
console.log(div.style.color); // red
</script>
NodeList
Nodelist、NamedNodeMap、HTMLCollection
NodeList
对象是节点的集合。一般通过属性和方法返回,
Node.childNodes
document.querySelectorAll()
NamedNodeMap
接口代表一个 Attr
对象集合。
HTMLCollection
返回元素元素节点的集合。以下属性和方法可以返回
Node.children
document.getElementsBy*
Live NodeLists
这三个集合都是“动态的”。换句话说,每当文档结构发生变化时,它们都会得到更新。它们始终都会保存着最新、最准确的信息。
所有 NodeList 对象都是在访问 DOM 文档时实时运行的查询。
<div>example</div>
var divs = document.getElementsByTagName("div"),
i,
len,
div;
// NodeList [div] length = 1
console.log(document.querySelectorAll('div');
// HTMLCollection [div] length = 2
console.log(document.getElementsByTagName('div'));
// len = divs.length
// 缓存 DOM 查询结果
for (i = 0, len = divs.length; i < len; i++) {
div = document.createElement('div');
div.textContent = 'example';
document.body.appendChild(div);
}
应该尽量减少访问 NodeList 的次数,因为每次访问 NodeList,都会运行一次基于文档的查询。所以,可以考虑将从 NodeList 中取得的值缓存起来。
Static NodeLists
document.querySelectorAll(selector)
方法返回一个静态的 NodeList
。在 DOM 中任何改变都不会影响此集合的内容。
Shadow DOM VS. Virtural DOM
- Shadow DOM is a browser technology designed primarily for scoping variables and CSS in web components.
- The Virtual DOM is a concept implemented by libraries in JavaScript on top of browser APIs.
HTMLTemplateElement
属性:content
,只读的 DocumentFragment
DocumentFragment
文档片段接口,一个没有父对象的最小文档对象。
- 不是真实DOM树的一部分,变化不会触发DOM树的重新绘制。
HTMLTemplateElement.content
属性包含一个DocumentFragment
- 可作为
Node.appendChild
和Node.insertBefore
方法的参数,插入片段所有子节点,片段本身不插入,且此操作只会发生一次重绘操作,不会导致性能问题。 - 该接口在 Web 组件中非常有用。
Node.cloneNode()
复制调用该方法节点的副本,添加到文档之前,此拷贝节点不属于当前文档树的一部分,没有父节点。
CSSOM API
CSS Rule tree 需要对照DOM Tree 从右向左解析CSS 的 选择器。
CSS匹配 HTML 元素是一个相当复杂和有性能问题的事情。所以,你就会在很多地方看到:DOM 树要小,CSS 尽量用 id 和 class,千万不要过渡层叠下去(避免冗余的元素嵌套)。
Reflow 成本远大于 Repaint
下面的动作会增加成本
- 增加,删除和修改 DOM 节点
- 移动 DOM 位置,制作动画等
- 修改 CSS 样式
- Resize 窗口,滚动
- 修改网页默认字体
display: none 会触发 reflow,visibility:hidden 只会触发 repaint,位置没有变化。
reflow 的原因:
- initial
- Incremental, JS 操作 DOM
- Resize, 尺寸变化
- StyleChange
- Dirty
减少 reflow/repaint
不要一条条地修改DOM的样式。
// bad
var left = 10;
var top = 10;
el.style.left = left + 'px';
el.style.top = top + 'px';
//Good
el.classList.add('className');
//Good
// Using el.style.cssTextDOM 离线后修改,使用documentFragment 对象在内存里操作DOM
const list = document.querySelector('.list');
const fruits = ['apple', 'banana', 'orange'];
const fragment = document.createDocumentFragment();
fruits.forEach(fruit => {
const li = document.createElement('li');
li.textContent = fruit;
fragment.appendChild(li)
})
list.appendChild(fragment);不要把DOM结点的属性值放在一个循环里当成循环里的变量。
尽可能的修改层级比较低的DOM。当然,改变层级比较底的DOM有可能会造成大面积的 reflow,但是也可能影响范围很小。
使用 position 为
fixed
或absoulte
的元素,修改它们的 CSS 代码不会 reflow。不要使用 table 布局