DOM编程和操作跨线程

获取任意元素

有很多 API

window.idxxx; // 或者直接idxxx
document.getElementById("idxxx");
document.getElementsByTagName("div")[0];
document.getElementsByClassName("red")[0];
document.querySelector("#idxxx"); // 记得加#
document.querySelectorAll(".red")[0];

用哪一个

工作中用querySelectorquerySelectorAll

例子: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 显示错了

  • 自身属性: classNameidstyle等等
  • 第一层原型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会被当做thisevent则包含了点击事件的所有信息,如坐标

  • div.addEventListener

    EventTarget.addEventListener() | MDN

改内容

  • 改文本内容
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 的标准属性的修改,会被浏览器同步到页面中,比如idclassNametitle

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 轮子

comments powered by Disqus