Skip to content
This page has been auto-translated and may contain errors.View in English

事件

docs.scrimba.com

一个永远不做出反应的页面只是一份文档。当你想要在用户点击按钮、在搜索框中输入内容或提交表单时发生某些事情时,你需要一种方式来说"当这种情况发生时,执行这段代码"。这就是事件。本章讲的是如何监听用户的操作并对其做出响应。

监听事件

一个**事件**是页面上发生的某件事:点击、按键、表单被提交。要对其做出响应,你需要选择一个元素、命名你关心的事件,并交给一个函数来处理它。

这个函数被称为处理器。将其连接起来的方法是 addEventListener

js
const button = document.querySelector(".buy-button");

button.addEventListener("click", () => {
  console.log("有人点击了购买按钮");
});

从左到右读这段代码:在这个按钮上,监听 "click" 事件,当它发生时,运行这个函数。处理器每次事件发生时都会运行,不是只运行一次。点击三次,它就运行三次。

一个**事件**是浏览器在某件事发生时触发的信号:用户点击、输入、提交表单,或者页面完成加载。你通过用 addEventListener 注册一个处理器来响应,它接受事件名称作为字符串和每次事件触发时调用的函数。

js
const button = document.querySelector(".buy-button");

const handleBuy = () => {
  console.log("有人点击了购买按钮");
};

button.addEventListener("click", handleBuy);

注意 handleBuy 是没有括号传递的。你是在把函数本身交给浏览器以便稍后调用,而不是现在就调用它。写 addEventListener("click", handleBuy()) 会立即调用它并注册它返回的任何内容,这是一个常见的早期错误。

一个**事件**是浏览器分发给任何已注册对它感兴趣的代码的消息。你用 addEventListener(type, handler) 注册:type 是事件名称字符串,handler 是浏览器在该元素上每次事件触发时调用的回调。

js
const button = document.querySelector(".buy-button");

const handleBuy = () => {
  console.log("有人点击了购买按钮");
};

button.addEventListener("click", handleBuy);

这里有两件事很重要。首先,你传递 handleBuy(函数值),而不是 handleBuy()(调用它的结果)。其次,addEventListener 是叠加的:用两个函数调用两次,两个都会运行,所以监听器是堆叠而不是覆盖的。这就是它存在的实际原因,而不是赋值给 button.onclick,后者只能保持一个处理器并会覆盖任何以前的。保持一个对处理器的命名引用也很重要,以便稍后当你想删除它时使用。

处理器是普通的函数,你附加它们的元素来自DOM。如果你能选择一个元素并写一个函数,你就可以对用户对它所做的事情做出响应。

Juno监听事件 要响应用户,选择一个元素,将事件命名为字符串,如 "click",并给 addEventListener 一个函数来运行。那个函数就是处理器,它每次事件发生时都会运行,不是一次。这一个方法是页面响应你的几乎所有事情的幕后推手。
Juno监听事件addEventListener 接受一个事件名称和一个函数,每次事件触发时调用那个函数。通过名称传递函数,不带括号:handleBuy,不是 handleBuy()。添加括号会立即调用它而不是在点击时调用,这几乎会绊倒每个人一次。
Juno监听事件addEventListener 注册浏览器在每个匹配事件上触发的回调,并且它是叠加而不是覆盖的,这就是你更喜欢它而不是 onclick 的原因。传递函数值,而不是它的调用结果,所以是 handleBuy 而绝不是 handleBuy()。也要保留一个命名引用,因为当你想删除监听器时会需要它。

事件对象

当浏览器运行你的处理器时,它给它传递一条信息:一个描述发生了什么的**事件对象**。通过给你的处理器一个参数来捕获它,通常命名为 event

js
const button = document.querySelector(".buy-button");

button.addEventListener("click", (event) => {
  console.log(event.target); // 被点击的元素
});

最有用的部分是 event.target:事件发生的元素。如果点击是在按钮上,event.target 就是那个按钮。你不必重新选择它,事件已经告诉你它来自哪里。

每个处理器都接收一个参数:事件对象,它携带关于发生了什么的详细信息。你通过命名一个参数来捕获它,常规上叫 event(或 e):

js
const search = document.querySelector(".search-input");

search.addEventListener("input", (event) => {
  console.log(event.target.value); // 用户到目前为止输入的内容
});

event.target 是事件触发的元素。在表单字段上它是明星,因为 event.target.value 给你输入框的当前文本。那个配对,input 事件加上 event.target.value,是你在用户输入时读取他们输入内容的方式。

浏览器向你的处理器传递一个参数,事件对象,它是发生了什么的快照:哪个元素、哪个键、鼠标位置,以及控制事件默认行为的方法。你用一个参数来捕获它,常规上叫 event

js
const search = document.querySelector(".search-input");

search.addEventListener("input", (event) => {
  console.log(event.target.value); // 字段中的当前文本
});

event.target 是发起事件的元素,这不总是你附加监听器的元素(那个是 event.currentTarget,它们之间的差异是事件委托工作的原理)。在表单控件上,event.target.value 读取当前值为字符串,所以 HTML 作为文本保存的同一个 .value 在这里也返回为文本。要获得一个数字,你可以用 Number(event.target.value) 自己转换它。

Juno事件对象 你的处理器得到一个参数,事件对象,所以给它一个命名为 event 的参数来捕获它。你将最常用的部分是 event.target,即事件发生的元素。无需重新选择它,事件已经知道它来自哪里。
Juno事件对象 命名一个参数 event,浏览器会用关于发生了什么的详细信息填充它。event.target 是它触发的元素,在输入框上,event.target.value 是字段中当前的文本。将 input 事件与 event.target.value 配对,你就可以在用户输入时读取他们的输入。
Juno事件对象 事件对象是发生了什么的快照,event.target 是它开始的地方,这不总是你附加监听器的地方;那个是 event.currentTarget。在表单字段上 event.target.value 返回为字符串,所以当你需要数字时用 Number(...) 包装它。记住 target-versus-currentTarget 的分割,因为事件委托依赖它。

常见事件

少数几个事件覆盖了你将构建的大部分内容:

  • click:用户点击或轻触某个东西。
  • input:文本字段的值在他们输入时改变。
  • submit:表单被提交。
  • keydown:一个键被按下。

棘手的是 submit。默认情况下,提交表单会重新加载页面,这会清除你的 JavaScript 正在做的任何事情。要停止这种情况,在处理器内调用 event.preventDefault()

js
const form = document.querySelector("form");

form.addEventListener("submit", (event) => {
  event.preventDefault(); // 停止页面重新加载
  console.log("表单已提交,页面保持不动");
});

大多数页面依赖于一小组事件:按钮的 click、随着文本字段变化的 input、表单的 submit 和键盘快捷键的 keydown。学习这四个,你就可以构建大多数交互。

让人困惑的是 submit。表单的默认行为是重新加载页面或导航离开,这会扔掉你的代码正在处理的任何东西。调用 event.preventDefault() 来保持页面在原地并自己接管:

js
const form = document.querySelector("form");

const handleSubmit = (event) => {
  event.preventDefault();
  const email = form.querySelector(".email-input").value;
  console.log(`注册用户 ${email}`);
};

form.addEventListener("submit", handleSubmit);

在表单本身上监听 submit,而不是在按钮上监听 click,这样按 Enter 的键盘用户也被覆盖了。

四个事件承载了大多数交互:clickinput(在每个值改变时触发,不像 change 那样等待失焦)、submitkeydown。将它们绑定到正确的元素,大多数 UI 就会由它们产生。

submit 事件是 preventDefault 发挥作用的地方。表单的默认操作是完整导航,这会丢弃你的页面内状态。调用 event.preventDefault() 取消它,所以你自己处理数据:

js
const form = document.querySelector("form");

const handleSubmit = (event) => {
  event.preventDefault();
  const email = form.querySelector(".email-input").value;
  console.log(`注册用户 ${email}`);
  // 将它发送到服务器是下一章的工作
};

form.addEventListener("submit", handleSubmit);

附加到表单的 submit,而不是按钮的 clicksubmit 对 Enter 键、按钮点击和编程提交都会触发,所以它是正确的钩子。一旦你有了值,将它发送到某处是异步代码的用武之地,这是下一章。

冒泡和委托

当你点击一个元素时,事件不会在那里停止。它**冒泡**:它在被点击的元素上触发,然后在它的父元素上,然后在那个父元素的父元素上,一直往上。这就是为什么在父元素上的监听器能听到它的子元素上的点击。

这个行为支持委托:将一个监听器附加到父元素,并使用 event.target 来找出实际点击的是哪个子元素。不是每个项目一个监听器,你得到覆盖所有项目的一个。

js
const list = document.querySelector(".todo-list");

list.addEventListener("click", (event) => {
  if (event.target.matches(".delete")) {
    event.target.closest("li").remove();
  }
});

好处是:这处理了稍后添加到列表的项目。逐项监听器只绑定现在存在的元素;委托给父元素的监听器捕获在代码运行时不存在的子元素上的点击,因为事件仍然冒泡到正在监听的父元素。

移除监听器

removeEventListener 分离一个处理器,但仅当你传递你添加的完全相同的函数引用时:

js
const button = document.querySelector(".buy-button");

button.addEventListener("click", handleBuy);
button.removeEventListener("click", handleBuy); // 有效:相同的引用

这就是为什么匿名处理器不能被移除。像 addEventListener("click", () => {...}) 这样的内联箭头函数每次都会创建一个新函数,所以你没有引用可以交给 removeEventListener,监听器就卡住了。你计划移除的命名处理器避免了这个。比监听器指向的元素更长存活的监听器是内存泄漏的真实来源,页面不再需要的对象因为监听器仍然指向它们而无法被回收。

还有一个性能说明:在滚动或触摸监听器上添加 { passive: true } 作为第三个参数承诺你不会调用 preventDefault,这让浏览器可以滚动而不用等待你的处理器,并保持滚动流畅。

Juno常见事件 你将最常使用的事件是 clickinputsubmitkeydown。要记住的是 submit:表单默认重新加载页面,所以在处理器中调用 event.preventDefault() 来停止那种行为并保持控制。如果你错过了,你的页面会在你下面刷新,第一次这会让人困惑。
Juno常见事件clickinputsubmitkeydown 覆盖了你构建的大部分内容。在表单上监听 submit,而不是在按钮上监听 click,这样 Enter 对键盘用户有效。并在提交处理器中调用 event.preventDefault(),否则页面会重新加载并带走你的状态。
Juno常见事件 事件冒泡到父元素,所以一个委托监听器加上 event.target 覆盖许多子元素,包括稍后添加的。removeEventListener 需要你添加的相同函数引用,这就是为什么匿名处理器永远无法被移除并且会泄漏内存。而且 submit 胜过按钮的 click 因为它也对 Enter 触发;在那里保留 event.preventDefault() 来停止重新加载。

从这里事件走向何处

事件是页面如何监听的方式。你选择一个元素,命名你关心的东西,并在它发生时运行一个函数,从事件对象中读取详细信息。几乎每个交互特性都是从那个循环构建的。

自然的下一步是在处理器中做一些缓慢的事情,比如将表单发送到服务器或加载新数据,而不冻结页面。这就是异步代码的用途,它是下一章。处理器进入结构化数据,比如你在对象中保持的项目列表,也依赖于你已经知道的东西。