事件
事件是元素传达变更的标准方式。这些变更通常由用户交互引起。例如,当用户点击按钮时,按钮会派发一个点击事件;当用户在输入框中输入值时,输入框会派发一个变更事件。
除了这些自动派发的标准事件外,Lit 元素还可以派发自定义事件。例如,菜单元素可能会在选中项变更时派发一个事件;弹出元素可能会在弹出窗口打开或关闭时派发事件。
任何 JavaScript 代码,包括 Lit 元素本身,都可以监听事件并根据事件采取行动。例如,工具栏元素可能会在选择菜单项时过滤列表;登录元素可能会在处理登录按钮的点击事件时处理登录。
除了标准的 addEventListener
API 外,Lit 还引入了一种声明式的方式来添加事件监听器。
在元素模板中添加事件监听器
Permalink to "在元素模板中添加事件监听器"你可以在模板中使用 @
表达式向组件模板中的元素添加事件监听器。声明式事件监听器在模板渲染时添加。
自定义事件监听器选项
Permalink to "自定义事件监听器选项"如果你需要为声明式事件监听器自定义事件选项(如 passive
或 capture
),你可以使用 @eventOptions
装饰器在监听器上指定这些选项。传递给 @eventOptions
的对象会作为 options
参数传递给 addEventListener
。
import {LitElement, html} from 'lit';
import {eventOptions} from 'lit/decorators.js';
//...
@eventOptions({passive: true})
private _handleTouchStart(e) { console.log(e.type) }
使用装饰器。 装饰器是一项提议中的 JavaScript 功能,因此你需要使用像 Babel 或 TypeScript 这样的编译器来使用装饰器。详情请参阅 启用装饰器。
如果你不使用装饰器,可以通过向事件监听器表达式传递一个对象来自定义事件监听器选项。该对象必须有一个 handleEvent()
方法,并且可以包含通常出现在 addEventListener()
的 options
参数中的任何选项。
render() {
return html`<button @click=${{handleEvent: () => this.onClick(), once: true}}>click</button>`
}
向组件或其影子根添加事件监听器
Permalink to "向组件或其影子根添加事件监听器"要接收来自组件槽内子元素以及通过组件模板渲染到影子 DOM 中的子元素派发的事件通知,你可以使用标准的 addEventListener
DOM 方法向组件本身添加监听器。有关完整详情,请参阅 MDN 上的 EventTarget.addEventListener()。
组件构造函数是在组件上添加事件监听器的好地方。
constructor() {
super();
this.addEventListener('click', (e) => console.log(e.type, e.target.localName));
}
向组件本身添加事件监听器是一种事件委托形式,可以用来减少代码量或提高性能。详情请参阅事件委托。通常这样做时,会使用事件的 target
属性根据触发事件的元素采取行动。
然而,当组件上的事件监听器接收到来自组件影子 DOM 的事件时,这些事件会被重新指向。这意味着事件目标是组件本身。有关更多信息,请参阅在影子 DOM 中处理事件。
重新指向可能会干扰事件委托,为了避免这种情况,可以向组件的影子根本身添加事件监听器。由于 shadowRoot
在 constructor
中不可用,因此可以在 createRenderRoot
方法中添加事件监听器,如下所示。请注意,从 createRenderRoot
方法返回影子根是很重要的。
向其他元素添加事件监听器
Permalink to "向其他元素添加事件监听器"如果你的组件向除自身或其模板化 DOM 之外的任何元素添加事件监听器(例如,向 Window
、Document
或主 DOM 中的某个元素),你应该在 connectedCallback
中添加监听器,并在 disconnectedCallback
中移除它。
在
disconnectedCallback
中移除事件监听器可确保当你的组件被销毁或从页面断开连接时,由你的组件分配的任何内存都会被清理。在
connectedCallback
中添加事件监听器(而不是,例如,在构造函数或firstUpdated
中)可确保你的组件在断开连接后随后重新连接到 DOM 时重新创建其事件监听器。
connectedCallback() {
super.connectedCallback();
window.addEventListener('resize', this._handleResize);
}
disconnectedCallback() {
window.removeEventListener('resize', this._handleResize);
super.disconnectedCallback();
}
有关使用自定义元素生命周期回调的更多信息,请参阅 MDN 文档中关于 connectedCallback
和 disconnectedCallback
的内容。
添加事件监听器非常快,通常不是性能问题。然而,对于高频使用并需要大量事件监听器的组件,你可以通过事件委托减少使用的监听器数量,并在渲染后异步添加监听器,来优化首次渲染性能。
使用事件委托可以减少使用的事件监听器数量,从而提高性能。集中事件处理来减少代码量有时也很方便。事件委托只能用于处理冒泡
的事件。有关冒泡的详细信息,请参阅派发事件。
冒泡事件可以在 DOM 中的任何祖先元素上被听到。你可以利用这一点,在祖先组件上添加单个事件监听器,以接收 DOM 中其任何后代派发的冒泡事件的通知。使用事件的 target
属性,根据派发事件的元素采取特定行动。
异步添加事件监听器
Permalink to "异步添加事件监听器"要在渲染后添加事件监听器,请使用 firstUpdated
方法。这是一个 Lit 生命周期回调,在组件首次更新并渲染其模板化 DOM 后运行。
firstUpdated
回调在你的组件第一次被更新并调用其 render
方法后触发,但在浏览器有机会绘制之前。
有关更多信息,请参阅生命周期文档中的 firstUpdated。
为确保在用户看到组件后添加监听器,你可以等待一个在浏览器绘制后解析的 Promise。
async firstUpdated() {
// 给浏览器一个绘制的机会
await new Promise((r) => setTimeout(r, 0));
this.addEventListener('click', this._handleClick);
}
理解事件监听器中的 this
Permalink to "理解事件监听器中的 this" 使用模板中的声明式 @
语法添加的事件监听器会自动_绑定_到组件。
因此,你可以在任何声明式事件处理程序中使用 this
来引用你的组件实例:
class MyElement extends LitElement {
render() {
return html`<button @click="${this._handleClick}">click</button>`;
}
_handleClick(e) {
console.log(this.prop);
}
}
当使用 addEventListener
命令式地添加监听器时,你会想要使用箭头函数,以便 this
引用组件:
export class MyElement extends LitElement {
private _handleResize = () => {
// `this` 引用组件
console.log(this.isConnected);
}
constructor() {
window.addEventListener('resize', this._handleResize);
}
}
有关更多信息,请参阅 MDN 上关于 this
的文档。
监听从重复模板触发的事件
Permalink to "监听从重复模板触发的事件"当监听重复项上的事件时,如果事件冒泡,使用事件委托通常很方便。当事件不冒泡时,可以在重复元素上添加监听器。以下是这两种方法的示例:
移除事件监听器
Permalink to "移除事件监听器"向 @
表达式传递 null
、undefined
或 nothing
将导致移除任何现有的监听器。
所有 DOM 节点都可以使用 dispatchEvent
方法派发事件。首先,创建一个事件实例,指定事件类型和选项。然后将其传递给 dispatchEvent
,如下所示:
const event = new Event('my-event', {bubbles: true, composed: true});
myElement.dispatchEvent(event);
bubbles
选项允许事件沿 DOM 树向上流动,到达派发元素的祖先。如果你希望事件能够参与事件委托,设置这个标志很重要。
composed
选项的设置很有用,它允许事件被派发到元素所在的影子 DOM 树之外。
有关更多信息,请参阅在影子 DOM 中处理事件。
有关派发事件的完整描述,请参阅 MDN 上的 EventTarget.dispatchEvent()。
何时派发事件
Permalink to "何时派发事件"事件应该在响应用户交互或组件状态的异步变化时派发。通常,它们不应该在响应组件所有者通过其属性或特性 API 所做的状态更改时派发。这通常是原生 Web 平台元素的工作方式。
例如,当用户在 input
元素中输入值时,会派发一个 change
事件,但如果代码设置 input
的 value
属性,则不会派发 change
事件。
类似地,菜单组件应该在用户选择菜单项时派发事件,但如果设置菜单的 selectedItem
属性,它不应派发事件。
这通常意味着组件应该在响应它正在监听的另一个事件时派发事件。
在元素更新后派发事件
Permalink to "在元素更新后派发事件"通常,事件应该仅在元素更新并渲染后触发。如果事件旨在传达基于用户交互的渲染状态变化,这可能是必要的。在这种情况下,可以在改变状态后但在派发事件之前等待组件的 updateComplete
Promise。
使用标准或自定义事件
Permalink to "使用标准或自定义事件"事件可以通过构造 Event
或 CustomEvent
来派发。两种方法都是合理的。当使用 CustomEvent
时,任何事件数据都会传递在事件的 detail
属性中。当使用 Event
时,可以创建事件子类并附加自定义 API。
有关构造事件的详细信息,请参阅 MDN 上的 Event。
触发自定义事件
Permalink to "触发自定义事件"const event = new CustomEvent('my-event', {
detail: {
message: 'Something important happened'
}
});
this.dispatchEvent(event);
有关自定义事件的更多信息,请参阅 MDN 关于自定义事件的文档。
触发标准事件
Permalink to "触发标准事件"class MyEvent extends Event {
constructor(message) {
super();
this.type = 'my-event';
this.message = message;
}
}
const event = new MyEvent('Something important happened');
this.dispatchEvent(event);
在影子 DOM 中处理事件
Permalink to "在影子 DOM 中处理事件"在使用影子 DOM 时,标准事件系统有一些修改,了解这些很重要。影子 DOM 主要存在是为了在 DOM 中提供一种作用域机制,封装这些"影子"元素的细节。因此,影子 DOM 中的事件从外部 DOM 元素中封装某些细节。
理解组合事件派发
Permalink to "理解组合事件派发"默认情况下,在影子根内派发的事件在该影子根外部不可见。要使事件穿过影子 DOM 边界,你必须将 composed
属性 设置为 true
。将 composed
与 bubbles
配对很常见,这样 DOM 树中的所有节点都可以看到该事件:
_dispatchMyEvent() {
let myEvent = new CustomEvent('my-event', {
detail: { message: 'my-event happened.' },
bubbles: true,
composed: true });
this.dispatchEvent(myEvent);
}
如果事件是 composed
且确实 bubble
,则它可以被派发事件的元素的所有祖先接收——包括外部影子根中的祖先。如果事件是 composed
但不 bubble
,则它只能被派发事件的元素和包含影子根的宿主元素接收。
请注意,大多数标准用户界面事件,包括所有鼠标、触摸和键盘事件,都是冒泡和组合的。有关更多信息,请参阅 MDN 关于组合事件的文档。
理解事件重定向
Permalink to "理解事件重定向"从影子根内部派发的组合事件会被重定向,这意味着对于托管影子根的元素或其任何祖先上的任何监听器来说,它们似乎来自托管元素。由于 Lit 组件渲染到影子根中,从 Lit 组件内部派发的所有组合事件似乎都是由 Lit 组件本身派发的。事件的 target
属性是 Lit 组件。
<my-element onClick="(e) => console.log(e.target)"></my-element>
render() {
return html`
<button id="mybutton" @click="${(e) => console.log(e.target)}">
click me
</button>`;
}
在需要确定事件起源的高级情况下,使用 event.composedPath()
API。这个方法返回事件派发过程中遍历的所有节点的数组,包括影子根内的节点。因为这会破坏封装,应该小心避免依赖可能暴露的实现细节。常见用例包括确定点击的元素是否为锚标签,用于客户端路由。
handleMyEvent(event) {
console.log('Origin: ', event.composedPath()[0]);
}
有关更多信息,请参阅 MDN 关于 composedPath 的文档。
事件派发者和监听者之间的通信
Permalink to "事件派发者和监听者之间的通信"事件主要存在是为了将变更从事件派发者传达给事件监听者,但事件也可以用于将信息从监听者传回派发者。
你可以做到这一点的一种方式是在事件上暴露 API,监听者可以使用这些 API 来自定义组件行为。例如,监听者可以在自定义事件的 detail 属性上设置一个属性,派发组件随后使用该属性来自定义行为。
派发者和监听者之间通信的另一种方式是通过 preventDefault()
方法。可以调用它来表示不应发生事件的标准操作。当监听者调用 preventDefault()
时,事件的 defaultPrevented
属性变为 true。然后监听者可以使用这个标志来自定义行为。
以下示例使用了这两种技术: