DOM编程和操作跨线程
获取任意元素
有很多 API
window.idxxx; // 或者直接idxxx
document.getElementById("idxxx");
document.getElementsByTagName("div")[0];
document.getElementsByClassName("red")[0];
document.querySelector("#idxxx"); // 记得加#
document.querySelectorAll(".red")[0];
用哪一个
工作中用
querySelector
和querySelectorAll
例子:
document.querySelectorAll('div>span:nth-child(2)')
做 demo 直接用
idxxx
,千万别让人发现 hhh
要兼容 IE 的可怜虫才用
getElement(s)ByXXX
获取特定元素
获取 html 元素
document.documentElement;
获取 head 元素
document.head;
获取 body 元素
document.body;
获取窗口
窗口不是元素
window;
获取所有元素
document.all; // false
这个 document.all 是个奇葩,第 6 个 falsy 值。原因是过去用来判断是否是 IE 浏览器。
元素的 6 层原型链
抓一只 div 对象来看看
console.dir(div1); // 查看原型链
Chrome 显示错了
- 自身属性:
className
、id
、style
等等 - 第一层原型HTMLDivElement.prototype
这里面是所有 div 共有的属性,不用细看
- 第二层原型HTMLElement.prototype
这里面是所有 HTML 标签共有的属性,不用细看
- 第三层原型
Element.prototype
这里面是所有 HTML 标签共有的属性,你不会以为浏览器只能展示 HTML 吧
- 第四层原型
Node.prototype
这里面是所有节点共有的属性,节点包括 XML 标签文本注释、HTML 标签文本注释等等
- 第五层原型
EventTarget.prototype
里面最重要的函数属性是
addEventListener
- 最后一层原型就是
Object.prototype
了
div 原型链
节点和元素的区别: 节点 Node 包括(Element 和 Text 等):Node.nodeType - Web API 接口参考 | MDN
元素的增删改查
增
- 创建一个标签节点
let div1 = document.createElement("div");
document.createElement("style");
document.createElement("script");
document.createElement("li");
- 创建一个文本节点
text1 = document.createTextNode("Hi there");
- 标签里面插入文本
div1.appendChild(text1);
div1.innerText = "Hi there"; // 或者 div1.textContent = 'Hi there'
// 但是不能用 div1.appendChild(‘Hi there’)
- 插入页面中
你创建的标签默认处于 JS 线程中,你必须把它插到 head 或者 body 里面,它才会生效
document.body.appendChild(div);
// 或者 已在页面中的元素.appendChild(div)
删
- 两种方法
// 旧方法
parentNode.removeChild(childNode); // 用父节点删子节点,很傻
// 新方法
childNode.remove();
如果一个 node 被移出页面(DOM 树),那么它还可以再次回到页面中吗?可以,自己试试。
改
改属性
- 写标准属性
// 改class
div.className = "red blue"; // 全覆盖, 旧的
// 改class
div.classList.add("red");
// 改style
div.style = "width:100px;color:blue"; // 旧的
// 改style的一部分
div.style.width = "200px";
// 注意大小写
div.style.backgroundColor = "white";
// 改data-*属性, 旧的
div.setAttribute("data-x", "test");
div.getAttribute("data-x");
div.dataset.x;
div.dataset.x = "Alice";
- 读标准属性
div.classList; // 或者 a.href
div.getAttribute("class"); // 或者 a.getAttribute("href")
// 第二种方法可以确保获得你想要的,比较保险
两种方法都可以,但值可能稍微有些不同
改事件处理函数
div.onclick
默认为null
默认点击
div
不会有任何事情发生,但是如果你把div.onclick
改为一个函数fn
,那么点击div
的时候,浏览器就会调用这个函数,并且是这样调用的:fn.call(div, event)
div
会被当做this
,event
则包含了点击事件的所有信息,如坐标div.addEventListener
改内容
- 改文本内容
div.innerText = "xxx";
div.textContent = "xxx";
两者几乎没有区别
- 改 HTML 内容
div.innerHTML = "<strong>content here</strong>";
- 改标签
div.innerHTML = ""; // 先清空
div.appendChild(div2); // 再加内容
改父节点
newParent.appendChild(div);
查
- 查父节点
node.parentNode; // 或者 node.parentElement
- 查祖先节点
node.parentNode.parentNode;
- 查子节点
node.childNodes; // 或者node.children
区别: 前面是包括文本节点的,后面是不包括文本节点的
- 查兄弟节点
node.parentNode.childNodes; // 还要排除自己
node.parentNode.children; // 还要排除自己
let siblings = [];
let arr = div2.parentElement.children; // 获得所有子节点
for (let i = 0; i < c.length; i++) {
if (arr[i] !== div2) {
// 排除自己
sibling.push(arr[i]);
}
}
- 查找首尾节点和其他特定兄弟节点
node.firstChild; // 查首节点
node.lastChild; // 查尾节点
node.previousSibling; // 查上一个兄弟节点
node.nextSibling; // 查下一个兄弟节点
- 遍历一个 div 里面的所有元素
travel = (node, fn) => {
fn(node);
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
travel(node.children[i], fn);
}
}
};
travel(div1, (node) => console.log(node));
DOM 操作跨线程
跨线程操作
各线程各司其职
- JS 引擎不能操作页面,只能操作 JS
- 渲染引擎不能操作 JS,只能操作页面
跨线程通信
document.body.appendChild(div1)
这句话是如何改变页面的?
- 当浏览器发现 JS 在 body 里面加了个 div1 对象
- 浏览器就会通知渲染引擎在页面里也新增一个 div 元素
- 新增的 div 元素所有属性都照抄 div1 对象
图示跨线程操作
插入新标签的完整过程
在 div1 放入页面之前
- 你对 div1 所有的操作都属于 JS 线程内的操作
let div1 = document.createElement("div");
div1.textContent = "hi";
把 div1 放入页面之时
document.body.appendChild(div1);
- 浏览器会发现 JS 的意图,就会通知渲染线程在页面中渲染 div1 对应的元素
把 div1 放入页面之后
- 你对 div1 的操作都有可能会触发重新渲染
- 如果你连续对 div1 多次操作,浏览器可能会合并成一次操作,也可能不会,详情见之前的博客: CSS 动画的原理和浏览器渲染原理
启示: 可以用’看似无用的代码', 比如获取一下页面的 width, 强制浏览器多次渲染。
属性同步
标准属性
- 对 div1 的标准属性的修改,会被浏览器同步到页面中,比如
id
、className
、title
等
data-*属性
同上
非标准属性
- 对非标准属性的修改,则只会停留在 JS 线程中,不会同步到页面里
比如属性: 示例代码
启示
- 如果你有自定义属性,又想被同步到页面中,请使用
data-
作为前缀
图示
左边的在
JS
里的属性,叫做properties
,右边的在HTML
里的属性,叫做attributes
。所以我们
getAttributes()
获取的是页面中的属性。
Property v.s. Attribute
property 属性
- JS 线程中 div1 对象的所有属性,叫做 div1 的 property
attribute 也是属性
- 渲染引擎中 div1 对应标签的属性,叫做 attribute
区别
- 大部分时候,同名的 property 和 attribute 值相等
- 但如果不是标准属性(把 div1 放入页面之后只有标准属性会同步),那么它俩只会在一开始时相等
- 但注意 attribute 只支持字符串
- 而 property 支持字符串、布尔等类型
网上都说 DOM 操作慢,实际上只是比 JS 操作慢,DOM 操作比网络请求还是快很多的。
关于这一部分内容,延伸阅读: Why’s the browser DOM still so slow after 10 years of effort
注意,网上的文章说的不一定都是对的,作为参考了解一下即可。
Q: 看了这么多,DOM 默认的 API 真 🐔 儿难用,怎么办?
A: 很简单,自己封装一个 DOM 库,自己试着造一个 DOM 轮子吧 :)
也可以参考下:我写的 DOM 轮子