`composed: true` 被认为有害吗?
composed: true被认为有害吗?
免责声明:有人指出,我为了吸引眼球,故意使用类似“标题党”的标题来提及大量“被认为有害”的文章,这可能会让人直接联想到一篇与此主题立场截然相反的经典文章。虽然我的确想用这样的标题吸引您点击进入此页面,但我并不认为自己对任何主题都拥有绝对权威的立场,更何况是像活动策划这样丰富多样的领域。如果您愿意与我展开对话,我衷心希望我们能进行一次有益的交流,并发现共同的知识基础是对话的最佳起点。那么,让我们开始吧!
首先,它到底是什么composed: true?什么时候会用到它?
Event.composed它定义了 DOM 事件是否会在事件分发所在的 Shadow DOM 和 Shadow 根元素所在的 Light DOM 之间传递。正如您在 MDN 相关文章中看到的那样,“所有通过 UA 分发的 UI 事件”默认都是组合的,但当您手动分发事件时,您可以根据需要设置此属性的值。因此,最composed: true简单的解释是“一种管理事件传输封装的方法”,而“何时使用”则是指“在使用 Shadow DOM 时”。虽然这种做法并非 Web 组件所独有,但它在某种程度上已成为 Web 组件的代名词,例如 Shadow DOM、自定义元素、ES6 模块和 `<div>` 元素<template>。接下来,在尝试就此做出决定之前,我们将回顾一些重要的概念composed: true:
到那时,我们都会成为这方面的专家,可以深入探讨一些关于 DOM 事件的实践和模式,这些实践和模式可能对你的应用程序中有用。我会分享一些我曾经有过或使用过的想法,也希望你能在下面的评论区分享你的经验。准备好了吗?
原生 DOM 事件
原生 HTML 元素通过 DOM 事件与 DOM 树进行通信。您可能经常看到 `<div>` 元素<input />发布 `<div>` 和 `<div>` 事件change,input或者 `<div> <button />` 元素发布 `<div>` 事件,而我们通常会依赖于click这些事件。您可能不会立即意识到自己依赖于这些事件,但当您应用onclick(原生)或(虚拟 DOM)属性时,底层正是依赖于这些 DOM 事件。由于这些事件会沿着 DOM 树分发,我们可以通过任何基于 `<div>` 的 DOM 节点上的方法,onChange选择监听这些事件的位置(可以是显式的,也可以是通用的)。addEventListener(type, listener[, options/useCapture])HTMLElement
这些事件分为两个阶段:“捕获”阶段和“冒泡”阶段。在捕获阶段,事件从 DOM 顶部向下传递到触发元素,可以通过将第三个参数设置为 true,或者在作为第三个参数传递的对象中addEventListener()显式包含 `is_dispatch_id`来监听事件在此阶段经过的每个元素。例如,以下 DOM 结构中事件的“捕获”阶段步骤如下:capture: trueoptionsclick<button>
<body>
<header>
<nav>
<button>Click me!</button>
</nav>
</header>
</body>
具体如下:
<body><header><nav><button>
然后,由于这是一个click事件,bubbles: true因此默认情况下会进行设置,所以该事件将进入“冒泡”阶段,并按以下顺序向上遍历 DOM,依次经过上面的 DOM:
<button><nav><header><body>
在监听此事件的任何阶段,您都可以访问 `onEvent` preventDefault()、 `onEvent`stopPropagation()和 ` stopImmediatePropagation()onEvent` 方法,从而对应用程序中传递的事件进行强大的控制。preventDefault()当监听 ` <a>` 标签click上的事件时,`onEvent` 的作用最为明显<a href="...">。在这种情况下,它会阻止锚链接被激活,并阻止页面导航。某种程度上,这是请求执行操作权限的事件,我们将结合手动分发的事件更详细地探讨这stopPropagation()一点。`onEvent` 会阻止相关事件沿着 DOM 树继续传递,并触发该路径上的后续监听器,这相当于在满足特定参数时为事件设置了一个“逃生阀”。通过 `onEvent`,可以进一步stopImmediatePropagation()阻止事件完成其所在阶段的当前步骤。这意味着不会调用同一 DOM 元素上后续绑定的针对该事件的监听器。回到<button>上面的示例元素,当click分发事件时,您可以想象以下几个非常简单的监听器:
const body = document.querySelector('body');
const header = document.querySelector('header');
const button = document.querySelector('button');
// You can hear the `click` event during the "capture" phase on the `<body>` element.
body.addEventListener('click', () => {
console.log('heard on `body` during "capture"');
}, true);
// You cannot hear the `click` event during the "bubble" phase on the `<body>` element.
body.addEventListener('click', () => {
console.log('not heard `body` during "bubble"');
});
// You can hear the `click` event during the "bubble" phase on the `<header>` element.
header.addEventListener('click', (e) => {
console.log('heard on `header` via listener 1 during "bubble"');
e.stopPropagation();
});
// You can hear the `click` event during the "bubble" phase on the `<header>` element.
header.addEventListener('click', (e) => {
console.log('heard on `header` via listener 2 during "bubble"');
e.stopImmediatePropagation();
});
// You cannot hear to the `click` event during the "bubble" phase on the `<header>`
// element being it is bound later than the previous listener and its use of the
// `stopImmediatePropagation()` method.
header.addEventListener('click', (e) => {
console.log('not heard on `header` via listener 3 during "bubble"');
});
// You can hear the `click` event during the "capture" phase on the `<button>` element.
button.addEventListener('click', () => {
coonsole.log('heard on `button` during "capture"');
}, true);
button.click();
// heard on `body` during "capture"
// heard on `button` during "capture"
// heard on `header` via listener 1 during "bubble"
// heard on `header` via listener 2 during "bubble"
大多数情况下,`is_returns` bubbles、cancelable`is_returns`(用于增强事件处理能力preventDefault())和` is_returns`的值composed在原生 DOM 事件中都是相同的,而且在很多情况下,`is_returns` 的值composed是true`false`,因此浏览器可能已经否定了它可能“有害”的想法。然而,在使用原生 DOM 事件时,这三个属性的值也是不可配置的。要获得相应的能力和责任,您需要进入手动分发事件的世界。
dispatchEvent()
到目前为止,我们主要讨论的是click浏览器自动分发的事件。当然,还有一整类由用户代理 (UA) 分发的 UI 事件可以用相同的方式处理(例如animationend`/ copy/ keydown/ mouseover/ / paste`touch等)。然而,真正的乐趣在于掌握主动权,开始分发自己创建的事件。为此,浏览器提供了一个dispatchEvent()方法,该方法适用于任何继承自 `<Event>` 的元素EventTarget,包括所有HTMLElement基于 `<Event>` 的 DOM 元素集合。为了让它发挥作用,我们需要提供一个要分发的事件。我们可以使用许多事件类来创建新的事件(例如 `<Event>` new Event()、new MouseEvent()`<Event>`、new InputEvent()`<Event>` 等),但 `<Event>` 本身就new Event(typeArg[, initDict])提供了非常广泛的可能性。
现在,我们准备发布一个事件。
el.dispatchEvent(new Event('test-event'));
事件已触发!
该事件具有一个type监听器test-event,因此直接在分发元素上设置的监听器将能够听到它:
el.addEventListener('test-event', (e) => console.log(e.type));
// test-event
您也可以在“捕获”阶段监听此事件:
const body = document.querySelector('body');
body.addEventListener('test-event', (e) => console.log(e.type), true);
// test-event
但是,在泡沫阶段你是听不到这种声音的:
const body = document.querySelector('body');
body.addEventListener('test-event', (e) => console.log(e.type));
// ... ... Bueller?
这是因为默认情况下,`a` new Event()(以及所有派生事件构造函数)都具有 `__init__` bubbles、 `__init__` 和 `__init__` cancelable,并且composed默认值为false`0`。这就是我们事件构造函数的可选initDict参数发挥作用的地方。当您想要自定义这些值时,您可以像这样创建事件:
const event = new Event('test-event', {
bubbles: true,
cancelable: true,
composed: true,
};
或者说,以最能支持(或者说损害最小?😉)特定用例的方式进行处理。这意味着,如果您只想让事件在“捕获”阶段可用(这实际上意味着它同步通过应用程序所需的时间比同时经过“冒泡”阶段要少一半),您可以将其省略。不需要执行任何操作?cancelable也可以省略。没有使用影子 DOM?确定它composed: true有害?这是您的选择,省略它!
防止违约
能够阻止手动触发的事件的默认行为真是太棒了。它允许你将应用程序中触发的操作构建成权限门。你的事件本质上是在询问“我是否有权限执行此操作?”,而无论答案是近在咫尺还是遥不可及,你都可以根据这些信息做出相应的响应。回到我们之前那个非常简单的示例 DOM:
<body>
<header>
<nav>
<button>Click me!</button>
</nav>
</header>
</body>
我们的按钮可能需要触发一个hover事件,cancelable: true以确保在当前查看上下文(由更中心的位置管理)中,该上下文适合显示hover内容或生成悬停相关的视觉效果,例如某些移动浏览器应该这样做,这样我们就无需点击两次即可使链接操作生效……在这种情况下,附加到该<body>元素的应用程序管理器将不会授予继续执行此操作的权限:
body.addEventListener('hover', e => e.preventDefault());
const event = new Event('hover', {
bubbles: true,
cancelable: true
});
const applyDefault = button.dispatchEvent(event);
console.log(applyDefault);
// false
console.log(event.defaultPrevented);
// true
这种模式不仅体现在原生锚点标签中,您可能还会在各种键盘事件以及其他许多方面注意到它。cancelable: true您可以选择与浏览器原生应用的模式和实践的契合程度。
自detail定义事件
事件能够清晰地表明某事已经发生(或即将发生),这本身就是一种强大的功能。然而,有时我们需要了解的信息远不止通过访问e.target(指向分发元素的引用)所能传达的,我们需要更清晰地了解事件,或者我们希望分发元素能够访问只有监听元素才能获取的信息。对于这种情况,现成的原生 UI 事件构造函数就显得不够用了。幸运的是,我们有两个非常棒的选择:`onEvent`new CustomEvent()和 `onEvent` class MyEvent extends Event {}。
自定义事件
new CustomEvent(typeArg[, initDict])它可以像我们之前讨论过的任何构造函数一样在您的应用程序中使用,并且由于其巧妙地命名为“自定义”事件,有时也被认为是创建手动分发事件的“唯一”接口。然而,此构造函数真正的强大之处在于它包含了 `<Event>` 属性detail。initDict虽然detail在创建事件后它不能直接写入,但可以将其设置为一个对象或数组,该对象或数组在被监听器修改时不会丢失标识。这意味着您不仅可以在分发事件时向其中追加数据,还可以在监听器中追加/编辑其中的数据,从而允许您使用事件来解析应用程序中更高层管理的数据值。准备好迎接另一个简单的示例,想象一下以下 HTML:
<body>
<header> ... </header>
<main>
<section>
<h1>Resolving title...</h1>
<h2>Resolving title...</h2>
</section>
</main>
</body>
从这里开始,我们的文本<h1>可以这样解决:
body.addEventListener('title', e => e.detail.tile = 'Hello, World!');
const event = new CustomEvent('title', {
bubbles: true,
detail: {
title: 'Failed to find a title.'
}
});
h1.dispatchEvent(event);
h1.innerText = event.detail.title;
这一切都得益于fordetail属性的可用性以及 DOM 事件是同步的这一事实(这意味着,当紧随其后的行运行时,事件已经遍历了其设置和监听器允许的每个 DOM 节点),这非常强大。initDictnew CustomEvent()dispatchEvent()
扩展事件
通过扩展基类,可以实现非常相似且更深入的自定义Event。这种方法可以直接访问原本需要通过事件传递的数据,而无需中间层detail。此外,instanceof这种方法真正的优势在于它能够使用……。回到上面示例中的 HTML,现在让我们解析两个标题元素的值:
class H1Title extends Event {
constructor(title = 'Failed to find a title.') {
super('title', {
bubbles: true
});
this.title = title;
}
}
class H2Title extends Event {
constructor(title = 'Failed to find a title.') {
super('title', {
bubbles: true
});
this.title = title;
}
}
body.addEventListener('title', e => {
if (e instanceof H1Title) {
e.title = 'Hello, World!';
} else if (e instanceof H2Title) {
e.title = 'We're going places.';
}
});
const h1Title = new H1Title();
const h2Title = new H2Title();
h1.dispatchEvent(h1Title);
h1.innerText = h1Title.title;
h2.dispatchEvent(h2Title);
h2.innerText = h2Title.title;
无论你采用哪种方法,使用 DOM 事件在应用程序中传递实际数据都非常强大。从上面简单的示例到更完整的 Context API 或 DOM 绑定的 Redux 实现,并非难事。这种方法还可以作为应用程序中异步操作的编排器。要了解更多关于如何利用这种事件的信息,请观看Justin Fagnani的精彩演讲:
来自暗影根部的事件
到目前为止,我们讨论的每个事件都是在没有影子根的文档中分发的。因此,我们无需考虑任何额外的封装,这意味着除非你利用影子根,stopPropagation()否则stopImmediatePropagation()在这些事件中,“捕获”阶段将覆盖从document分发元素到整个 DOM 树,而bubbles: true“冒泡”阶段则会反向执行相同的操作。当影子根附加到元素时,它会创建一个与主文档 DOM 树隔离的子 DOM 树。如前所述,大多数由用户代理 (UA) 分发的 UI 事件composed: true默认具有影子根,并且可以在子树和主树之间随意传递。现在我们已经了解了如何手动分发事件,我们可以选择我们创建的事件是否也具有影子根。
事件重定向
在此之前,我们先来看看当事件composed: true在影子根中被触发时会发生什么,这种情况会经常发生(包括 UA 触发的 UI 事件)。例如,考虑以下 DOM 树中的某个元素触发的click事件(默认情况下也具有bubbles: true`@Return` 属性) :<button>
<document>
<body>
<div>
<shadow-root-el>
#shadow-root
<div>
<button>
Click here!
</button> <!-- click happens here -->
</div>
</shadow-root-el>
</div>
</body>
</document>
与轻量级 DOM 中的事件类似,click此处的事件也会在 `<div>` 元素开始其“捕获”阶段<document>。然而,轻量级 DOM 和影子 DOM 事件的第一个区别就在这里显现出来:target此事件的 `<div>` 元素并非 `<div>`<button>元素。正如影子根元素的<shadow-root-el>设计初衷,它会将 DOM 封装在其子树内,并将其隐藏起来,使其无法被实现该 DOM 的文档识别。这样一来,它便会将相关的事件重定向到 ` <shadow-root-el><div>` 元素。
<document> <!-- event: `click`, phase: "capture", target: `shadow-root-el` -->
<body>
<div>
<shadow-root-el>
#shadow-root
<div>
<button>
Click here!
</button> <!-- click happens here -->
</div>
</shadow-root-el>
</div>
</body>
</document>
该事件将使用这些设置向下遍历 DOM 树,直到到达影子根节点。在影子根节点,我们将体验到轻量级 DOM 事件和影子 DOM 事件之间的下一个区别。影子根节点是我们子树中第一个封装了内部结构的节点,这<shadow-root-el>意味着我们位于封装的 DOM内部,内部结构不再对我们隐藏。在这里,事件target将显式地发生<button>在该元素上click。
<document>
<body>
<div>
<shadow-root-el>
#shadow-root <!-- event: `click`, phase: "capture", target: `button` -->
<div>
<button>
Click here!
</button> <!-- click happens here -->
</div>
</shadow-root-el>
</div>
</body>
</document>
从这里开始,事件仍处于“捕获”阶段,将继续向下遍历 DOM,直到到达其target根节点<button>。此时,它将处于“捕获”阶段。同时,在向上遍历 DOM 之前,它也会作为“冒泡”阶段的第一步出现。
<document>
<body>
<div>
<shadow-root-el>
#shadow-root
<div>
<button>
<!-- event: `click`, phase: "capture", target: `button` -->
<!-- event: `click`, phase: "bubble", target: `button` -->
Click here!
</button> <!-- click happens here -->
</div>
</shadow-root-el>
</div>
</body>
</document>
在“冒泡”阶段,事件会像在“捕获”阶段一样,受到封装效应的影响。虽然事件经过影子根节点后的目标元素会是 `<div>`<button>元素(从 `<div>` 开始<shadow-root-el>),但事件会在继续向上冒泡之前,被重新定向到该元素。
<document>
<body>
<div>
<shadow-root-el> <!-- event: `click`, phase: "bubble", target: `shadow-root-el` -->
#shadow-root <!-- event: `click`, phase: "bubble", target: `button` -->
<div>
<button>
Click here!
</button> <!-- click happens here -->
</div>
</shadow-root-el>
</div>
</body>
</document>
扩展重定向
当使用嵌套的影子根元素(例如,自定义元素内部包含自定义元素)时,事件重定向会在事件遇到的每个影子边界处发生。这意味着,如果事件经过三个影子根元素,则重定向target将发生三次:
<body> <-- target: parent-el -->
<parent-el> <-- target: parent-el -->
#shadow-root <-- target: child-el -->
<child-el> <-- target: child-el -->
#shadow-root <-- target: grandchild-el -->
<grandchild-el> <-- target: grandchild-el -->
#shadow-root <-- target: button -->
<button> <-- target: button -->
Click here!
</button> <!-- click happens here -->
<grandchild-el>
<child-el>
<parent-el>
</body>
当然,这就是影子根提供的封装功能的优势之一:影子根中发生的一切都会留在影子根中,或者至少看起来是这样。
人迹罕至的沉稳之路
有时候,我们需要深入探究事件的源头,无论是 `<div>` <button>、<div>`<div>` 、 `<div><a> `,还是其他什么(希望是 `<div>`<button>或`<div> <a>`……无障碍访问,各位!)。而对于这种情况,我们可以使用事件的 `get_response` 方法。在事件生命周期的任何阶段,调用该事件都会返回一个数组,其中包含所有可能接收到该事件的 DOM 元素。该数组按“冒泡”顺序排列(即使 `<div>` 为 `<div> `),因此第 0 个元素是分发元素,最后一个元素是事件传递的最后一个元素。这意味着,您可以始终使用以下代码来确定原始分发元素,并概述事件的传递路径(假设前面的示例 HTML 代码相同):composedPath()composedPath()bubbles: false
const composedPath = e.composedPath()
const originalDispatchingElement = composedPath[0];
console.log(composedPath);
// [
button,
document-fragment,
grandchild-el,
document-fragment,
child-el,
document-fragment,
parent-el,
body, html,
document,
window
]
正是在这里,composedPath()这种影响composed: true体现得最为明显。当一个事件具有 `<dispatch>`属性时composed: true,该路径将从原始分发元素一直延伸到window包含整个事件的 ` <shadowroot>` document;但当一个事件具有 `<dispatch>` 属性时,composed: false该路径将终止于包含分发元素的 `<shadowroot>`。
分解事件
正如我们目前所见,composed: true事件的作用在于使其尽可能地像原生 DOM 事件一样运作:允许其“捕获”阶段从文档根目录(以及中间的影子边界)开始,并进入原始触发元素所在的影子 DOM 子树,然后再允许“冒泡”阶段反向执行相同的操作。在此过程中,事件会受到其经过的影子根的影响,并被重新定向到该影子根所附着的元素。事件composed: true在影子根中的行为与不在影子根中时还有一点不同:composed: true允许事件穿过影子根后,它会在影子根所附着的元素上触发(如同处于“冒泡”阶段,但不会向上遍历 DOM)。这意味着(参考下面的 DOM 结构),虽然在“捕获”阶段composed: true, bubbles: false触发的事件<event-dispatching-element>会经过以下代码中的所有元素,但<shadow-root-el>在“冒泡”阶段,只有 `<div>` 元素会触发该事件。
<div>
<shadow-root-el>
#shadow-root
<section>
<div>
<event-dispatching-element>
所以,这确实composed: false为我们带来了新的、有趣的功能。
当使用 `dispatch` 函数分发事件时,composed: false该事件将被包含在触发它的影子根元素内。对于追求速度的开发者来说,这意味着事件处理速度会更快!`dispatch` 函数{bubbles: false}可以通过完全消除“冒泡”阶段(即事件所需传播距离的一半)来使事件速度翻倍,而 ` {composed: false}dispatch` 函数则可以将传播距离缩短到两步:分发元素和包含它的影子根元素(假设 DOM 树结构如此简化)。代码速度可能并非此处关注的重点,尽管值得一提。真正重要的是访问权限。当使用 `dispatch` 函数分发事件时,composed: false只有封装在同一个影子根元素中的祖先元素才能访问该事件。
是的,Shadow DOM 不仅允许你封装 CSS、DOM 和 JavaScript,它还会为你封装事件,本质上将元素变成了一个封闭的应用程序生态系统。在你的子树中,你可以分发任意数量的事件,事件名称可以简单(取决于其包含的作用域),也可以复杂(取决于它们是否是公共的),根据需要在内部处理它们,然后仅在需要(或准备就绪)时,才将一个新的、文档清晰且明确封装的事件分发到父作用域。该父作用域也可以是一个 Shadow DOM 树,它可以对分发到其中的各种事件执行相同的操作。将这种方法一直向上推演,就能非常清楚地看到 Shadow DOM 如何通过这种封装真正增强组件的重用性。composed: false是DOM 事件的私有字段。
责任部分
那么,我们该如何看待这种力量?它又会给我们带来什么样的麻烦?毕竟,像“composed: true有害”这样笼统的断言背后的前提是,它迟早会给我们带来麻烦。
我着手研究这种风险的缘由,始于一场关于处理事件时传递回调函数和通过监听器处理事件之间细微差别的讨论。使用传递回调函数时,你知道接下来还需要做一些工作:
const doWork = () => console.log('Do work.');
然后将其传递给需要执行该工作的元素。
const primaryButton = ({onClick}) => html`
<button @click=${onClick}>Primary Button</button>
`;
render(primaryButton({onClick: doWork}), document.body);
这样,如果需要,您就可以从很远的地方传递此回调函数:
const doWork = () => console.log('Do work.');
class PrimaryButton extend LitElement {
static get properties() {
return {
onClick: { type: Function, attribute: false}
};
}
render() {
return html`
<button @click=${this.onClick}>Primary Button</button>
`;
}
}
customElements.define('primary-button', PrimaryButton);
class Card extend LitElement {
static get properties() {
return {
doWork: { type: Function, attribute: false}
};
}
render() {
return html`
<div class="card">
<h1>Something</h1>
<p>Some stuff...</p>
<primary-button .onClick=${this.doWork}></primary-button>
</div>
`;
}
}
customElements.define('custom-card', Card);
class Section extend LitElement {
static get properties() {
return {
doWork: { type: Function, attribute: false}
};
}
render() {
return html`
<section>
<custom-card .doWork=${this.doWork}></custom-card>
</section>
`;
}
}
customElements.define('custom-section', section);
render(html`<custom-section .doWork=${doWork}></custom-section>`, document.body);
但最终,所有工作都在事件发生地完成。这样,即使你知道某些工作可能需要在应用程序的高层执行,你也可以使用模板系统(在上面的例子中lit-html是通过 `<div> LitElement`,但也可以通过各种虚拟 DOM 系统实现)将该操作传递到事件发生地。这种方法与 `<div>` 配合使用效果极佳,composed: false因为通过传递给<primary-button>元素的回调,只有<button>元素本身需要知道正在分发的事件。然而,我们刚刚了解到click事件(以及大多数其他默认 UI 事件)是通过 `<div>` 分发的composed: true,这意味着我们也可以这样做:
const doWork = () => console.log('Do work.');
class PrimaryButton extend LitElement {
render() {
return html`
<button>Primary Button</button>
`;
}
}
customElements.define('primary-button', PrimaryButton);
class Card extend LitElement {
render() {
return html`
<div class="card">
<h1>Something</h1>
<p>Some stuff...</p>
<primary-button></primary-button>
</div>
`;
}
}
customElements.define('custom-card', Card);
class Section extend LitElement {
render() {
return html`
<section>
<custom-card></custom-card>
</section>
`;
}
}
customElements.define('custom-section', section);
render(html`<custom-section @click=${doWork}></custom-section>`, document.body);
在上面的例子中,我们监听了事件,这是因为该click事件composed: true默认具有监听功能。理论上,两种代码示例输出的用户体验相同,但事实并非如此。传递回调的示例只会在元素内部的元素被点击doWork时调用,而监听的示例不仅会在元素内部的元素被点击时调用,还会在元素的任何其他部分被点击时调用,例如 `<div>` 、`<span>` 、`<span> ` 等。这就是“被认为有害”的原因。虽然监听事件可以让你更轻松地监听目标事件,但它也会监听到比你预期更多的事件。通过传递回调的方法,你还可以更进一步,利用我们讨论过的方法,阻止那些自然处于事件生命周期后期阶段的 DOM 元素监听该事件:<button><primary-button>doWork<custom-section><p><h1><div>composed: truecomposed: truestopPropagation()
const doWork = (e) => {
e.stopPropagation();
console.log('Do work.');
}
我们现在感觉安全了吗?!
非标准事件
事件click(通常指所有事件MouseEvents)在这方面非常强大:它们可以发生在任何地方。如果不传递回调函数,你就只能依赖事件委托来控制这些影响范围广、来源广泛的事件的影响。虽然这看起来很强大(而且在一个非常流行的合成事件系统中也得到了应用),但它本质上破坏了由自定义元素界定的影子 DOM 边界所提供的封装性。也就是说,如果你必须知道某个元素<custom-section>有一个<custom-card>子元素,该<primary-button>子元素又有一个<button>子元素,如此循环往复才能响应点击事件,那么一开始为什么要进行封装呢?所以,这种做法composed: true最终是有害的吗?我想听听你的想法,但同时我们也需要考虑以下几点。当我们手动分发事件时,我们可以决定这些事件的名称。
我们所有的非标准事件,无论通过 ` new Event('custom-name')onEvent`、`onEvent`new CustomEvent('custom-name')还是 `onEvent`发起class CustomNamedEvent extends Event { constructor() { super('custom-name'); } },都完全由我们掌控。这意味着我们不再需要担心click事件的通用性,可以使用自定义命名系统来分发更具体的importing-thing-you-care-about事件名称(例如 `onEvent`)。通过这种方式,我们可以更好地控制对事件的响应:
render(html`<custom-section @importing-thing-you-care-about=${doWork}></custom-section>`, document.body);
在这种情况下,我们可以相当肯定,只有我们预期会触发importing-thing-you-care-about事件的对象才会触发事件。通过这种方法,我们可以远程监听,并确保只有我们预期会触发事件的元素才会触发事件,而无需借助事件委托之类的技术。也许这意味着我们一直以来composed: true都把“事件委托”和“事件监听”混淆了……在这种情况下使用“事件监听”是否composed: true安全?这最终取决于你的应用程序的具体需求。
概要
- DOM 事件非常强大(即使像我们今天这样只关注
bubbles、cancelable和composed设置),可以在应用程序中用于许多事情。bubbles控制事件是否进入其生命周期的后半段或“泡沫”阶段cancelable允许preventDefault()向调度单元发送批准信号composed决定事件与影子 DOM 边界的关系
composed: true如果您以前使用过这些事件(无论是在 shadow DOM 中还是其他情况下),您可能已经习惯了几乎所有这些事件默认包含的方式。composed: true这使得事件可以被远距离监听,因此事件的命名变得更加重要。- 当向组件传递事件回调时,
composed: false可以对应用程序响应该事件的能力进行细粒度控制。
composed: true被认为有害吗?
有了这些新知识,你认为什么才算composed: true有害?浏览器默认将所有用户代理分发的 UI 事件设置为 true,这是否在暗中损害我们的利益composed: true?也许 truecomposed: true适用于“应用程序”,而composed: falsefalse 适用于“组件”……但是,我们应该如何划定界限?虽然我在手动分发事件时使用过这两种值,但我可以说我更composed倾向于使用 false ,这主要是因为缺乏反思,而不是出于规划。综上所述,很难说哪一种更好/更危险。如果你花时间观看了上面那段信息量丰富的视频,你会看到大量在构建 Web 应用时使用 false 的优质案例。也许false 并非有害?我确信的一点是,与大多数技术决策一样,你为 false 设置的值应该根据你的应用程序和/或相关组件的具体需求来决定。然而,我的经验仅仅是我的经验而已。我很想听听你的故事!请在下方评论区留言,分享你是否曾受到伤害以及如何受到伤害。composed: true composed: truecomposed: truecomposedcomposed: true
想做更多研究吗?
还在努力理解这一切究竟是怎么回事吗?我搭建了一个活动测试环境,你可以在这里测试我们目前讨论过的各种设置和实际情况:
虽然其中的设计可能存在一些问题,但希望它能帮助你更清楚地理解可以应用于事件的设置,以及这些设置如何影响事件在 DOM 中的传递方式。请注意,每个接收到事件的 DOM 元素都会发出通知,并记录接收到事件的阶段、事件经过该元素的路径步骤,以及该位置与原始事件target分发元素相邻的元素。我在我的应用程序和基于 Shadow DOM 的组件中大量使用手动分发的事件,而编写这个小程序极大地巩固了我对 DOM 事件的理解(也让我在某些方面感到惊讶),所以希望它也能对你有所帮助。随着你学习的深入,如果你对这个项目进行修改以帮助你阐述自己的想法composed: true,请在下面的评论中与我们分享。
文章来源:https://dev.to/open-wc/composed-true-considered-harmful-5g59后记:这篇文章最初的阅读时间只有3分钟,我发誓!我当时的想法是,速战速决,让大家讨论一些重要的事情。但一些比较冷静的人指出,DOM事件并非简单的概念,即使是经验丰富的用户(包括我自己)的知识体系也可能存在缺陷,所以我进行了扩充。正因如此,这篇文章包含的信息量很大,如果您认为我遗漏了什么,请务必告诉我!我的目标是理清大家的知识脉络,而不是让情况变得更糟,您的帮助将不胜感激。我还要感谢Open Web Components团队,感谢他们冷静地指出问题所在,并为本文进行了大量的编辑工作。