你正在查看 Lit 的旧版本文档。点击 这里查看最新版本。

事件

事件是元素通信变化的标准方式。这些变化通常由用户交互引起。例如,按钮在用户点击时派发 click 事件;输入框在用户输入值时派发 change 事件。

除了这些自动派发的标准事件外,Lit 元素还可以派发自定义事件。例如,菜单元素可能派发一个事件来指示选中的项目已更改;弹出元素可能在弹出窗口打开或关闭时派发事件。

任何 JavaScript 代码,包括 Lit 元素本身,都可以监听事件并基于事件采取行动。例如,工具栏元素可能在菜单项被选中时过滤列表;登录元素可能在处理登录按钮点击时处理登录。

除了标准的 addEventListener API 外,Lit 还引入了一种声明式的方式来添加事件监听器。

在元素模板中添加事件监听器

Permalink to "在元素模板中添加事件监听器"

你可以在模板中使用 @ 表达式来为组件模板中的元素添加事件监听器。声明式事件监听器在模板渲染时被添加。

如果你需要自定义声明式事件监听器使用的事件选项(如 passivecapture),你可以使用 @eventOptions 装饰器在监听器上指定这些选项。传递给 @eventOptions 的对象作为 addEventListeneroptions 参数传递。

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>`
}

为组件或其 shadow root 添加事件监听器

Permalink to "为组件或其 shadow root 添加事件监听器"

要接收从组件的 slotted 子元素以及通过组件模板渲染到 Shadow DOM 中的子元素派发的事件通知,你可以使用标准的 addEventListener DOM 方法在组件本身上添加监听器。详见 MDN 上的 EventTarget.addEventListener()

组件构造函数是在组件上添加事件监听器的好地方。

constructor() {
super();
this.addEventListener('click', (e) => console.log(e.type, e.target.localName));
}

在组件本身上添加事件监听器是事件委托的一种形式,可以用来减少代码或提高性能。详见事件委托。通常这样做时,使用事件的 target 属性来基于哪个元素触发了事件来采取行动。

然而,从组件的 Shadow DOM 触发的事件在被组件上的事件监听器听到时会被重新定向。这意味着事件目标是组件本身。详见在 Shadow DOM 中处理事件

重新定向会干扰事件委托,为了避免它,事件监听器可以添加到组件的 shadow root 本身。由于 shadowRootconstructor 中不可用,事件监听器可以在 createRenderRoot 方法中如下添加。请注意,确保从 createRenderRoot 方法返回 shadow root 很重要。

为其他元素添加事件监听器

Permalink to "为其他元素添加事件监听器"

如果你的组件将事件监听器添加到除自身或其模板 DOM 以外的任何地方——例如,添加到 WindowDocument 或主 DOM 中的某个元素——你应该在 connectedCallback 中添加监听器,并在 disconnectedCallback 中移除它。

  • disconnectedCallback 中移除事件监听器确保当你的组件被销毁或从页面断开连接时,你的组件分配的任何内存都会被清理。

  • connectedCallback 中添加事件监听器(而不是在构造函数或 firstUpdated 中)确保如果你的组件断开连接并随后重新连接到 DOM,它会重新创建其事件监听器。

connectedCallback() {
super.connectedCallback();
window.addEventListener('resize', this._handleResize);
}
disconnectedCallback() {
window.removeEventListener('resize', this._handleResize);
super.disconnectedCallback();
}

有关 connectedCallbackdisconnectedCallback 的更多信息,请参阅 MDN 上关于使用自定义元素生命周期回调的文档。

添加事件监听器非常快,通常不是性能问题。然而,对于高频使用且需要大量事件监听器的组件,你可以通过事件委托减少使用的监听器数量,并在渲染后异步添加监听器来优化首次渲染性能。

使用事件委托可以减少使用的事件监听器数量,从而提高性能。集中事件处理以减少代码有时也很方便。事件委托只能用于处理会冒泡的事件。有关冒泡的详情,请参阅派发事件

冒泡事件可以在 DOM 中的任何祖先元素上听到。你可以利用这一点,在祖先组件上添加单个事件监听器来接收其 DOM 中任何后代派发的冒泡事件。使用事件的 target 属性来基于派发事件的元素采取具体行动。

要在渲染后添加事件监听器,请使用 firstUpdated 方法。这是 Lit 的一个生命周期回调,在组件首次更新并渲染其模板 DOM 后运行。

firstUpdated 回调在你的组件第一次更新并调用其 render 方法之后、但在浏览器有机会绘制之前触发。

有关更多信息,请参阅生命周期文档中的 firstUpdated

要确保监听器在用户可以看到组件后添加,你可以 await 一个在浏览器绘制后解决的 Promise。

async firstUpdated() {
// 给浏览器一个绘制的机会
await new Promise((r) => setTimeout(r, 0));
this.addEventListener('click', this._handleClick);
}

使用模板中的声明式 @ 语法添加的事件监听器会自动_绑定_到组件。

因此,你可以在任何声明式事件处理器中使用 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 "监听从重复模板触发的事件"

当在重复项目上监听事件时,如果事件会冒泡,使用事件委托通常很方便。当事件不会冒泡时,可以在重复元素上添加监听器。以下是两种方法的示例:

@ 表达式传递 nullundefinednothing 将导致任何现有的监听器被移除。

所有 DOM 节点都可以使用 dispatchEvent 方法派发事件。首先,创建一个事件实例,指定事件类型和选项。然后将其传递给 dispatchEvent,如下所示:

const event = new Event('my-event', {bubbles: true, composed: true});
myElement.dispatchEvent(event);

bubbles 选项允许事件沿 DOM 树向上流动到派发元素的祖先。如果你想让事件能够参与事件委托,设置此标志很重要。

composed 选项允许事件在元素所在的 Shadow DOM 树之外派发。

详见在 Shadow DOM 中处理事件

有关派发事件的完整描述,请参阅 MDN 上的 EventTarget.dispatchEvent()

事件应响应用户交互或组件状态的异步变化来派发。它们通常不应响应组件的所有者通过其属性或 attribute API 所做的状态更改来派发。这通常是原生 Web 平台元素的工作方式。

例如,当用户在 input 元素中输入值时会派发 change 事件,但如果代码设置了 inputvalue 属性,则不会派发 change 事件。

同样,菜单组件应该在用户选择菜单项时派发事件,但如果设置了菜单的 selectedItem 属性,则不应派发事件。

这通常意味着组件应该在响应它正在监听的另一个事件时派发事件。

通常,事件应仅在元素更新和渲染后触发。如果事件旨在传达基于用户交互的渲染状态变化,则可能需要这样做。在这种情况下,在更改状态后但在派发事件之前,可以 await 组件的 updateComplete Promise。

使用标准事件或自定义事件

Permalink to "使用标准事件或自定义事件"

事件可以通过构造 EventCustomEvent 来派发。两种都是合理的方法。使用 CustomEvent 时,任何事件数据都通过事件的 detail 属性传递。使用 Event 时,可以创建事件子类并附加自定义 API。

有关构造事件的详情,请参阅 MDN 上的 Event

const event = new CustomEvent('my-event', {
detail: {
message: 'Something important happened'
}
});
this.dispatchEvent(event);

详见 MDN 上的自定义事件文档

class MyEvent extends Event {
constructor(message) {
super();
this.type = 'my-event';
this.message = message;
}
}

const event = new MyEvent('Something important happened');
this.dispatchEvent(event);

使用 Shadow DOM 时,标准事件系统有一些重要的修改需要理解。Shadow DOM 的存在主要是为了在 DOM 中提供一种作用域机制来封装这些"shadow"元素的细节。因此,Shadow DOM 中的事件会对外部 DOM 元素封装某些细节。

默认情况下,在 shadow root 内部派发的事件在该 shadow root 外部不可见。要使事件通过 Shadow DOM 边界,你必须将 composed 属性设置为 true。通常将 composedbubbles 配对,以便 DOM 树中的所有节点都能看到该事件:

_dispatchMyEvent() {
let myEvent = new CustomEvent('my-event', {
detail: { message: 'my-event happened.' },
bubbles: true,
composed: true });
this.dispatchEvent(myEvent);
}

如果一个事件是 composed 并且会 bubble,它可以被派发事件的元素的所有祖先接收——包括外部 shadow root 中的祖先。如果一个事件是 composed 但不会 bubble,它只能在派发事件的元素和包含 shadow root 的宿主元素上被接收。

请注意,大多数标准用户界面事件,包括所有鼠标、触摸和键盘事件,既是冒泡的也是 composed 的。详见 MDN 上的 composed 事件文档

从 shadow root 内部派发的 composed 事件会被重新定向,这意味着对于托管 shadow root 的元素或其任何祖先上的任何监听器,它们看起来像是来自托管元素。由于 Lit 组件渲染到 shadow root 中,从 Lit 组件内部派发的所有 composed 事件看起来都是由 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。此方法返回事件派发所经过的所有节点的数组,包括 shadow root 内的节点。由于这破坏了封装,应注意避免依赖可能暴露的实现细节。常见用例包括确定被点击的元素是否是锚点标签,用于客户端路由。

handleMyEvent(event) {
console.log('Origin: ', event.composedPath()[0]);
}

详见 MDN 上的 composedPath 文档

事件派发者和监听者之间的通信

Permalink to "事件派发者和监听者之间的通信"

事件的存在主要是为了将变化从事件派发者传达给事件监听者,但事件也可以用于将信息从监听者传回给派发者。

一种方法是在事件上暴露 API,监听者可以使用它来自定义组件行为。例如,监听者可以在自定义事件的 detail 属性上设置一个属性,然后派发事件的组件使用它来自定义行为。

另一种在派发者和监听者之间通信的方式是通过 preventDefault() 方法。它可以被调用来表示不应执行事件的标准操作。当监听者调用 preventDefault() 时,事件的 defaultPrevented 属性变为 true。然后此标志可以被派发者用来自定义行为。

以下示例中使用了这两种技术: