
2025-04-09 日报 Day151

今日的鸡汤
一寸光阴一寸金,寸金难买寸光阴,时间买不到也租不来,惜时如金就是增进财富储量,因时制宜就是掌控发展变量,分秒必争就是创造价值增量。
今日学习内容
1、JS 红皮书 P651-662 第二十章:JavaScript API
今日笔记
1、 影子DOM: 概念上讲,影子 DOM(shadow DOM) Web 组件相当直观,通过它可以将一个完整的 DOM 树作为节点添加到父 DOM 树。这样可以实现 DOM 封装,意味着 CSS 样式和 CSS 选择符可以限制在影子 DOM子树而不是整个顶级 DOM 树中。
影子 DOM 与 HTML 模板很相似,因为它们都是类似 document 的结构,并允许与顶级 DOM 有一定程度的分离。不过,影子 DOM 与 HTML 模板还是有区别的,主要表现在影子 DOM 的内容会实际渲染到页面上,而 HTML 模板的内容不会。
- 理解影子DOM: 假设有以下 HTML 标记,其中包含多个类似的 DOM 子树:
Make me red!
Make me blue!
Make me green!
- 创建影子DOM: 考虑到安全及避免影子 DOM 冲突,并非所有元素都可以包含影子 DOM。尝试给无效元素或者已经有了影子 DOM 的元素添加影子 DOM 会导致抛出错误。
影子 DOM 是通过 attachShadow()方法创建并添加给有效 HTML 元素的。容纳影子 DOM 的元素被称为影子宿主(shadow host)。影子 DOM 的根节点被称为影子根(shadow root)。
attachShadow()方法需要一个shadowRootInit 对象,返回影子DOM的实例。shadowRootInit对象必须包含一个 mode 属性,值为”open”或”closed”。对”open”影子 DOM的引用可以通过 shadowRoot属性在 HTML 元素上获得,而对”closed”影子 DOM 的引用无法这样获取。
下面的代码演示了不同 mode 的区别:
1 | document.body.innerHTML = ` |
- 使用影子DOM: 把影子 DOM 添加到元素之后,可以像使用常规 DOM 一样使用影子 DOM。来看下面的例子,这里重新创建了前面红/绿/蓝子树的示例:虽然这里使用相同的选择符应用了 3 种不同的颜色,但每个选择符只会把样式应用到它们所在的影子 DOM 上。为此,3 个
1
2
3
4
5
6
7
8
9
10
11
12
13for (let color of ['red', 'green', 'blue']) {
const div = document.createElement('div');
const shadowDOM = div.attachShadow({ mode: 'open' });
document.body.appendChild(div);
shadowDOM.innerHTML = `
<p>Make me ${color}</p>
<style>
p {
color: ${color};
}
</style>
`;
}元素会出现 3 种不同的颜色。
可以这样验证这些元素分别位于它们自己的影子 DOM 中:在浏览器开发者工具中可以更清楚地看到影子 DOM。例如,前面的例子在浏览器检查窗口中会显示成这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23for (let color of ['red', 'green', 'blue']) {
const div = document.createElement('div');
const shadowDOM = div.attachShadow({ mode: 'open' });
document.body.appendChild(div);
shadowDOM.innerHTML = `
<p>Make me ${color}</p>
<style>
p {
color: ${color};
}
</style>
`;
}
function countP(node) {
console.log(node.querySelectorAll('p').length);
}
countP(document); // 0
for (let element of document.querySelectorAll('div')) {
countP(element.shadowRoot);
}
// 1
// 1
// 1影子 DOM 并非铁板一块。HTML 元素可以在 DOM 树间无限制移动:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29<body>
<div>
#shadow-root (open)
<p>Make me red!</p>
<style>
p {
color: red;
}
</style>
</div>
<div>
#shadow-root (open)
<p>Make me green!</p>
<style>
p {
color: green;
}
</style>
</div>
<div>
#shadow-root (open)
<p>Make me blue!</p>
<style>
p {
color: blue;
}
</style>
</div>
</body> - 合成与影子DOM槽位: 影子 DOM 是为自定义 Web 组件设计的,为此需要支持嵌套 DOM 片段。从概念上讲,可以这么说:位于影子宿主中的 HTML需要一种机制以渲染到影子 DOM中去,但这些 HTML又不必属于影子 DOM树。
默认情况下,嵌套内容会隐藏。来看下面的例子,其中的文本在 1000 毫秒后会被隐藏:为了显示文本内容,需要使用1
2
3
4
5
6document.body.innerHTML = `
<div>
<p>Foo</p>
</div>
`;
setTimeout(() => document.querySelector('div').attachShadow({ mode: 'open' }), 1000);标签指示浏览器在哪里放置原来的 HTML。下面的代码修改了前面的例子,让影子宿主中的文本出现在了影子 DOM 中: 现在,投射进去的内容就像自己存在于影子 DOM 中一样。检查页面会发现原来的内容实际上替代了1
2
3
4
5
6
7
8
9
10document.body.innerHTML = `
<div id="foo">
<p>Foo</p>
</div>
`;
document.querySelector('div')
.attachShadow({ mode: 'open' })
.innerHTML = `<div id="bar">
<slot></slot>
<div>`: 注意,虽然在页面检查窗口中看到内容在影子 DOM中,但这实际上只是 DOM内容的投射(projection)。实际的元素仍然处于外部 DOM 中:1
2
3
4
5
6
7
8<body>
<div id="foo">
#shadow-root (open)
<div id="bar">
<p>Foo</p>
</div>
</div>
</body>1
2
3
4
5
6
7
8
9
10
11
12
13document.body.innerHTML = `
<div id="foo">
<p>Foo</p>
</div>
`;
document.querySelector('div')
.attachShadow({ mode: 'open' })
.innerHTML = `
<div id="bar">
<slot></slot>
</div>`
console.log(document.querySelector('p').parentElement);
// <div id="foo"></div> - 事件重定向: 如果影子 DOM 中发生了浏览器事件(如 click),那么浏览器需要一种方式以让父 DOM 处理事件。不过,实现也必须考虑影子 DOM 的边界。为此,事件会逃出影子 DOM 并经过事件重定向(event retarget)在外部被处理。逃出后,事件就好像是由影子宿主本身而非真正的包装元素触发的一样。下面的代码演示了这个过程:2、自定义元素: 自定义元素为 HTML 元素引入了面向对象编程的风格。基于这种风格,可以创建自定义的、复杂的和可重用的元素,而且只要使用简单的 HTML 标签或属性就可以创建相应的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13// 创建一个元素作为影子宿主
document.body.innerHTML = `
<div onclick="console.log('Handled outside:', event.target)"></div>
`;
// 添加影子 DOM 并向其中插入 HTML
document.querySelector('div')
.attachShadow({ mode: 'open' })
.innerHTML = `
<button onclick="console.log('Handled inside:', event.target)">Foo</button>
`;
// 点击按钮时:
// Handled inside: <button onclick="..."></button>
// Handled outside: <div onclick="..."></div> - 创建自定义元素: 浏览器会尝试将无法识别的元素作为通用元素整合进 DOM。调用 customElements.define()方法可以创建自定义元素。下面的代码创建了一个简单的自定义元素,这个元素继承 HTMLElement:
1
2
3
4document.body.innerHTML = `
<x-foo >I'm inside a nonsense element.</x-foo >
`;
console.log(document.querySelector('x-foo') instanceof HTMLElement); // true自定义元素的威力源自类定义。例如,可以通过调用自定义元素的构造函数来控制这个类在 DOM中每个实例的行为:1
2
3
4
5
6class FooElement extends HTMLElement {}
customElements.define('x-foo', FooElement);
document.body.innerHTML = `
<x-foo >I'm inside a nonsense element.</x-foo >
`;
console.log(document.querySelector('x-foo') instanceof FooElement); // true注意 在自定义元素的构造函数中必须始终先调用 super()。如果元素继承了 HTMLElement或相似类型而不会覆盖构造函数,则没有必要调用 super(),因为原型构造函数默认会做这件事。很少有创建自定义元素而不继承 HTMLElement 的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class FooElement extends HTMLElement {
constructor() {
super();
console.log('x-foo')
}
}
customElements.define('x-foo', FooElement);
document.body.innerHTML = `
<x-foo></x-foo>
<x-foo></x-foo>
<x-foo></x-foo>
`;
// x-foo
// x-foo
// x-foo
如果自定义元素继承了一个元素类,那么可以使用 is 属性和 extends 选项将标签指定为该自定义元素的实例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class FooElement extends HTMLDivElement {
constructor() {
super();
console.log('x-foo')
}
}
customElements.define('x-foo', FooElement, { extends: 'div' });
document.body.innerHTML = `
<div is="x-foo"></div>
<div is="x-foo"></div>
<div is="x-foo"></div>
`;
// x-foo
// x-foo
// x-foo - 添加Web组件内容: 因为每次将自定义元素添加到 DOM 中都会调用其类构造函数,所以很容易自动给自定义元素添加子 DOM 内容。虽然不能在构造函数中添加子 DOM(会抛出 DOMException),但可以为自定义元素添加影子 DOM 并将内容添加到这个影子 DOM 中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class FooElement extends HTMLElement {
constructor() {
super();
// this 引用 Web 组件节点
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<p>I'm inside a custom element!</p>
`;
}
}
customElements.define('x-foo', FooElement);
document.body.innerHTML += `<x-foo></x-foo`;
// 结果 DOM:
// <body>
// <x-foo>
// #shadow-root (open)
// <p>I'm inside a custom element!</p>
// <x-foo>
// </body> - 使用自定义元素生命周期方法: 可以在自定义元素的不同生命周期执行代码。带有相应名称的自定义元素类的实例方法会在不同生命周期阶段被调用。自定义元素有以下 5 个生命周期方法。
constructor():在创建元素实例或将已有 DOM 元素升级为自定义元素时调用。
connectedCallback():在每次将这个自定义元素实例添加到 DOM 中时调用。
disconnectedCallback():在每次将这个自定义元素实例从 DOM 中移除时调用。
attributeChangedCallback():在每次可观察属性的值发生变化时调用。在元素实例初始化时,初始值的定义也算一次变化。
adoptedCallback():在通过 document.adoptNode()将这个自定义元素实例移动到新文档对象时调用。
下面的例子演示了这些构建、连接和断开连接的回调:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class FooElement extends HTMLElement {
constructor() {
super();
console.log('ctor');
}
connectedCallback() {
console.log('connected');
}
disconnectedCallback() {
console.log('disconnected');
}
}
customElements.define('x-foo', FooElement);
const fooElement = document.createElement('x-foo');
// ctor
document.body.appendChild(fooElement);
// connected
document.body.removeChild(fooElement);
// disconnected - 反射自定义元素属性: 自定义元素既是 DOM 实体又是 JavaScript 对象,因此两者之间应该同步变化。换句话说,对 DOM的修改应该反映到 JavaScript 对象,反之亦然。要从 JavaScript 对象反射到 DOM,常见的方式是使用获取函数和设置函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16document.body.innerHTML = `<x-foo></x-foo>`;
class FooElement extends HTMLElement {
constructor() {
super();
this.bar = true;
}
get bar() {
return this.getAttribute('bar');
}
set bar(value) {
this.setAttribute('bar', value)
}
}
customElements.define('x-foo', FooElement);
console.log(document.body.innerHTML);
// <x-foo bar="true"></x-foo> - 升级自定义元素: 并非始终可以先定义自定义元素,然后再在 DOM 中使用相应的元素标签。为解决这个先后次序问题,Web 组件在 CustomElementRegistry 上额外暴露了一些方法。这些方法可以用来检测自定义元素是否定义完成,然后可以用它来升级已有元素。