目录
*1. 向DOM中注入HTML
1.1 将HTNL字符串转换成DOM
- 转换的步骤如下所示:
- 确保HTML字符串是合法有效的
- 将它包裹在任意符合浏览器规则要求的闭合标签中
- 使用innerHTML将这串HTML插入到虚拟的DOM元素中
- 提取该DOM节点
预处理HTML源字符串
// 确保自闭合元素被正确解释
// 单标签
const tags = /^(area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i;
// 通过正则把错误的单标签转换为标签对
function convert(html) {
return html.replace(/(<(w+)[^>]*?)/>/g, (all, front, tag) => {
return tags.test(tag) ? all : front + "></" + tag + ">";
});
}
console.log(convert("<a/>"));
// <a></a>
console.log(convert("<hr />"));
// <hr />
包装HTML
- 根据HTML语义,一些HTML元素必须包装在某些容器元素中。有两种方式可以解决(都需要构建问题元素和容器之间的映射关系)
- 通过innnerHTML将该字符串直接注入到它的特定父元素中,该父元素提前使用内置的document.creatElemnet创建好
- HTML字符串可以在使用对应父元素包装后,直接注入到任意容器元素中
- 需要包装在其他元素中的元素
元素名称 | 父级元素 |
---|---|
<option>, <optgroup> | <select multiple>...</select> |
<legend> | <fieldset>...</fieldset> |
<thead>, <tbody>, <tfoot>, <colgroup>, <caption> |
<table>...</table> |
<tr> | <table><thead>...</thead></table> <table><tbody>...</tbody></table> <table><tfoot>...</tfoot></table> |
<td>, <th> | <table><tbody><tr>...</tr></tbody></table> |
<col> | <table> <tbody></tbody> <colgroup>...</colgroup> </table> |
使用具有multiple属性的<select>元素,因为它不会自动检查任何包含在其中的选项
对<col>的兼容处理需要一个额外的,否则<colgroup>不能正确生成
// 将元素标签转换为一系列DOM节点
function getNodes(htmlString, doc) {
// 需要特殊父级容器的元素映射表。
// 每个条目包含新节点的深度,以及父元素的HTML头尾片段
const map = {
"<td": [3, "<table><tbody><tr>", "</tr></tbody></table>"],
"<th": [3, "<table><tbody><tr>", "</tr></tbody></table>"],
"<tr": [2, "<table><thead>", "</thead></table>"],
"<option": [1, "<select multiple>", "</select>"],
"<optgroup": [1, "<select multiple>", "</select>"],
"<thead": [1, "<table>", "</table>"],
"<tbody": [1, "<table>", "</table>"],
"<tfoot": [1, "<table>", "</table>"],
"<colgroup": [1, "<table>", "</table>"],
"<caption": [1, "<table>", "</table>"],
"<col": [2, "<table><tbody></tbody><colgroup>", "</colgroup></table>"]
}
const tagName = htmlString.match(/<w+/);
let mapEntry = tagName ? map[tagName[0]] : null;
// 如果映射表中有匹配,使用匹配结果
// 如果没有,则构造空的父标记,深度为0作为结果
if (!mapEntry) { mapEntry = [0, "", ""] }
// 创建用于包含新节点的元素,如果传入了文档对象,则使用传入的,否则使用当前的
let div = (doc || document).createElement("div");
// 使用匹配得到的父级容器元素,包装后注入新创建的元素中
div.innerHTML = mapEntry[1] + htmlString + mapEntry[2];
// 参照映射关系定义的深度,向下遍历刚刚创建的DOM树,最终得到新创建的元素
while (mapEntry[0]--) {
div = div.lastChild;
}
// 返回新创建的元素
return div.childNodes;
}
1.2 将DOM元素插入到文档中
// 新增frgment参数,新增节点将被添加到这个DOM片段中
function getNodes(htmlString, doc, fragment) {
const map = {
"<td": [3, "<table><tbody><tr>", "</tr></tbody></table>"],
"<th": [3, "<table><tbody><tr>", "</tr></tbody></table>"],
"<tr": [2, "<table><thead>", "</thead></table>"],
"<option": [1, "<select multiple>", "</select>"],
"<optgroup": [1, "<select multiple>", "</select>"],
"<thead": [1, "<table>", "</table>"],
"<tbody": [1, "<table>", "</table>"],
"<tfoot": [1, "<table>", "</table>"],
"<colgroup": [1, "<table>", "</table>"],
"<caption": [1, "<table>", "</table>"],
"<col": [2, "<table><tbody></tbody><colgroup>", "</colgroup></table>"]
}
const tagName = htmlString.match(/<w+/);
let mapEntry = tagName ? map[tagName[0]] : null;
if (!mapEntry) { mapEntry = [0, "", ""] }
let div = (doc || document).createElement("div");
div.innerHTML = mapEntry[1] + htmlString + mapEntry[2];
while (mapEntry[0]--) {
div = div.lastChild;
}
// 添加节点到DOM片段中
if (fragment) {
while (div.firstChild) {
fragment.appendChild(div.firstChild);
}
}
return div.childNodes;
}
<div id="test"><b>Hello</b>, I'm Wango!</div>
<div id="test2"></div>
<script>
document.addEventListener("DOMContentLoaded", () => {
// 在DOM的国歌位置插入DOM片段
function insert(elems, args, callback) {
if (elems.length) {
const doc = elems[0].ownerDocument || elems[0];
const fragment = doc.createDocumentFragment();
const scripts = getNodes(args, doc, fragment);
const first = fragment.firstChild;
if (first) {
for (let i =0; elems[i]; i++) {
callback.call(elems[i], i > 0 ? fragment.cloneNode(true) : fragment);
}
}
}
}
const divs = document.querySelectorAll("div");
insert(divs, "<b>Name:</b>", function(fragment) {
this.appendChild(fragment);
});
insert(divs, "<span>First</span><span>Last</span>", function (fragment) {
this.parentNode.insertBefore(fragment, this);
});
});
</script>
2. DOM的特性和属性
通过DOM方法和属性访问特性值
<div></div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const div = document.querySelector("div");
// HTML DOM的原生特性,通常能被属性表示
div.setAttribute("id", "news-01");
console.log(div.id);
// news-01
console.log(div.getAttribute("id"));
// news-01
div.id = "news-02";
console.log(div.getAttribute("id"));
// news-02
// 但自定义特性不能被元素属性表示,需要使用
// setAttribute()和getAttribute()
div.setAttribute("data-news", "breaking");
console.log(div.getAttribute("data-news"));
// breaking
});
</script>
在HTML5中,为遵循规范,建议使用data-作为自定义属性的前缀,方便区分自定义特性和原生特性
3. 令人头疼的样式特性
常用的style元素属性是一个对象,该对象的属性与元素标签内指定的样式相对应。
3.1 样式在何处
<style>
div {
font-size: 1.8em;
border: 0 solid gold;
}
</style>
<div style="color: #000;" title="Hello"></div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const div = document.querySelector("div");
// 内联样式被记录
console.log(div.style.color);
// rgb(0, 0, 0)
// <style>标签内定义的的样式没有被记录
console.log(div.style.fontSize === "1.8em");
// false
console.log(div.style.borderWidth === "0");
// false
// 样式对象中不反应从CSS样式表中继承的任何样式信息
// 新赋值的样式被记录
div.style.borderWidth = "10px";
console.log(div.style.borderWidth);
// 10px
});
</script>
内联样式中的任何值,都优先于样式表继承的值(即使样式表规则使用!important的注释)
3.2 样式属性命名
一种访问样式的简单方法
<div style="color: red;font-size: 10px;background-color: #eee;"></div>
<script>
// 处理样式函数
// 如果传入value,将相应样式属性值赋值为value
// 如果没有传入value,则返回改样式属性值
// 可以通过它来设置/读取样式属性
function style(elem, key, value) {
// 将属性名转为驼峰格式
// 以同时兼容驼峰式和连字符式样式名
key = key.replace(/-([a-z])/ig, (all, letter) => {
return letter.toUpperCase();
});
// 如果传入value则将相应样式属性值设置为value
if (typeof value !== "undefined") {
elem.style[key] = value;
}
return elem.style[key];
}
document.addEventListener("DOMContentLoaded", () => {
const div = document.querySelector("div");
// 设置属性
style(div, "font-size", "20px");
style(div, "background-color", "#000");
console.log(div.style.fontSize === "20px");
// true
console.log(div.style.backgroundColor === "rgb(0, 0, 0)");
// true
// 读取属性
console.log(style(div, "font-size"));
// 20px
console.log(style(div, "background-color"));
// rgb(0, 0, 0)
});
</script>
3.3 获取计算后样式
一个元素的计算后样式(computed style)都是应用在该元素上的所有样式的组合,这些样式包括样式表、元素的style内联样式、、浏览器内置样式、JS脚本对style所作的各种操作等
<style>
div {
background-color: #ffc;
display: inline;
font-size: 1.8em;
border: 1px solid crimson;
color: green;
}
</style>
<div style="color: crimson;" id="test" title="hello"></div>
<script>
// 用于获取元素计算后属性
function fetchComputedStyle(elem, property) {
// getComputedStyle是浏览器提供的全局函数,可直接调用
const computedStyle = getComputedStyle(elem);
if (computedStyle) {
// 将传入的样式名转换为中横线分割
// 以同时兼容驼峰式和连字符式样式名
property = property.replace(/([A-Z])/g, "-$1".toLowerCase());
// getComputedStyle返回的对象提供了getPropertyValue方法
// 这个方法接收中横线分割格式的样式名
return computedStyle.getPropertyValue(property);
}
}
document.addEventListener("DOMContentLoaded", () => {
const div = document.querySelector("div");
console.log(fetchComputedStyle(div, "background-color"));
// rgb(255, 255, 204)
console.log(fetchComputedStyle(div, "color"));
// rgb(220, 20, 60) 返回的是内联样式中color的值,内联样式将css样式覆盖了
console.log(fetchComputedStyle(div, "borderWidth"));
// 1px
console.log(fetchComputedStyle(div, "borderTop"));
// 1px solid rgb(220, 20, 60)
});
</script>
3.4 测量元素的高度和宽度
- height和width的默认值都是auto,所以无法获取准确的值
- 使用offsetHeight和offsetWidth,但这两个值包含了padding值
- 隐藏元素(display: none)没有尺寸,offsetHeight和offsetWidth为0
- 获取隐藏元素在非隐藏状态下的尺寸可以先取消隐藏,获取值,再隐藏,具体为:
- 将display设置为block(可以获取值了,但元素会可见)
- 将visibility设置为hidden(使元素不可见,但元素位置会显示一个空白)
- 将position设置为absolute(将元素移出正常的可视区)
- 获取元素尺寸
- 恢复先前更改的属性
<div id="div1" style="display: none; 100px;height: 200px;background-color: #000;"></div>
<div id="div2" style=" 300px;height: 400px;background-color: #00ff00;"></div>
<script>
(function(scope) { // 使用立即执行函数创建私有作用域
const PROPERTIES = {
position: "absolute",
visibility: "hidden",
display: "block"
}
scope.getDimensions = elem => {
const previous = {}; // 用于保存原有属性值
for (let key in PROPERTIES) {
previous[key] = elem.style[key]; // 保存原有值
elem.style[key] = PROPERTIES[key]; // 替换设置
}
const results = { // 保存结果
elem.offsetWidth,
height: elem.offsetHeight
}
for (let key in PROPERTIES) { // 还原设置
elem.style[key] = previous[key];
}
return results;
}
})(window);
document.addEventListener("DOMContentLoaded", () => {
const div1 = document.getElementById("div1");
const div2 = document.getElementById("div2");
console.log(getDimensions(div1).height);
// 200
console.log(getDimensions(div1).width);
// 100
console.log(getDimensions(div2).height);
// 400
console.log(getDimensions(div2).width);
// 300
});
</script>
检查offsetHeight和offsetWidth属性值是否为0,可以非常有效地确定一个元素的可见性
4. 避免布局抖动
- 抖动原因:代码对DOM执行一系列(通常是不必要的)连续的读取和写入时(浏览器执行大量重新计算),浏览器无法优化布局操作
引起布局抖动的API和属性
接口对象 | 属性名 |
---|---|
Element | clientHeight, clientLeft, clientTop, clientWidth, focus, getBoundingClientRect, getClientRects, innerText, offsetHeight. offsetLeft, offsetParent, offsetTop, offsetWidth, outerText, scrollByLines, scrollByPages, scrollHeight, scrollIntoView, scroollIntoViewIfNeeded, scrollLeft, scrollTop, scrollWidth |
MouseEvent | layerX, layerY, offsetX, offsetY |
Window | getComputedStyle, scrollBy, scrollTo, scroll, scrollY |
Frame, Document, Image |
height, width |
<div id="div1">Hello</div>
<div id="div2">World</div>
<div id="div3">!!!!!!!!</div>
<script>
// 获取元素
const div1 = document.getElementById("div1");
const div2 = document.getElementById("div2");
const div3 = document.getElementById("div3");
// 执行一系列来纳许的读写操作,修改DOM使得布局失效
const div1Width = div1.clientWidth;
div1.style.width = div1Width/2 + "px";
const div2Width = div2.clientWidth;
div2.style.width = div2Width/2 + "px";
const div3Width = div3.clientWidth;
div3.style.width = div3Width/2 + "px";
// 防抖的一种方法:批量读写
// 批量读取所有布局属性
const div1Width = div1.clientWidth;
const div2Width = div2.clientWidth;
const div3Width = div3.clientWidth;
// 批量写入所有布局属性
div1.style.width = div1Width/2 + "px";
div2.style.width = div2Width/2 + "px";
div3.style.width = div3Width/2 + "px";
</script>