简化事件处理程序背后的思考

Avatar of Tiger Oakes
Tiger Oakes

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费积分!

事件 用于响应用户点击某个位置、使用键盘将焦点放在链接上以及更改表单中的文本。 当我第一次开始学习 JavaScript 时,我写了复杂的事件监听器。 最近,我学会了如何减少我编写的代码量和所需的监听器数量。

让我们从一个简单的例子开始:几个可拖动的盒子。 我们希望向用户显示他们拖动了哪个颜色的盒子。

<section>
  <div id="red" draggable="true">
    <span>R</span>
  </div>
  <div id="yellow" draggable="true">
    <span>Y</span>
  </div>
  <div id="green" draggable="true">
    <span>G</span>
  </div>
</section>

<p id="dragged">Drag a box</p>

查看 CodePen 上的
Dragstart 事件
,作者是 Tiger Oakes (@NotWoods)
CodePen 上。

直观的做法

当我第一次开始学习 JavaScript 事件时,我为每个元素编写了单独的事件监听器函数。 这是一个常见的模式,因为它是最简单的开始方式。 我们希望每个元素都有特定的行为,因此我们可以对每个元素使用特定的代码。

document.querySelector('#red').addEventListener('dragstart', evt => {
  document.querySelector('#dragged').textContent = 'Dragged red';
});

document.querySelector('#yellow').addEventListener('dragstart', evt => {
  document.querySelector('#dragged').textContent = 'Dragged yellow';
});

document.querySelector('#green').addEventListener('dragstart', evt => {
  document.querySelector('#dragged').textContent = 'Dragged green';
});

减少重复代码

该示例中的事件监听器非常相似:每个函数都显示一些文本。 这些重复代码可以折叠成一个辅助函数。

function preview(color) {
  document.querySelector('#dragged').textContent = `Dragged ${color}`;
}

document
  .querySelector('#red')
  .addEventListener('dragstart', evt => preview('red'));
document
  .querySelector('#yellow')
  .addEventListener('dragstart', evt => preview('yellow'));
document
  .querySelector('#green')
  .addEventListener('dragstart', evt => preview('green'));

这样更简洁,但它仍然需要多个函数和事件监听器。

利用事件对象

Event 对象是简化监听器的关键。 当调用事件监听器时,它还会将 Event 对象作为第一个参数发送。 该对象包含一些描述发生的事件的数据,例如事件发生的时间。 为了简化我们的代码,我们可以使用 evt.currentTarget 属性,其中 currentTarget 指的是附加事件监听器的元素。 在我们的示例中,它将是三个彩色框之一。

const preview = evt => {
  const color = evt.currentTarget.id;
  document.querySelector('#dragged').textContent = `Dragged ${color}`;
};

document.querySelector('#red').addEventListener('dragstart', preview);
document.querySelector('#yellow').addEventListener('dragstart', preview);
document.querySelector('#green').addEventListener('dragstart', preview);

现在只有一个函数而不是四个。 我们可以将完全相同的函数重新用作事件监听器,并且 evt.currentTarget.id 将具有不同的值,具体取决于触发事件的元素。

使用冒泡

最后一个更改是减少代码中的行数。 与其为每个框附加一个事件监听器,我们可以为包含所有彩色框的 <section> 元素附加一个事件监听器。

事件从事件发起的元素(其中一个框)开始。 但是,它不会止步于此。 浏览器将转到该元素的每个父级,并调用其上的任何事件监听器。 这样一直持续到到达文档的(HTML 中的 <body> 标签)为止。 此过程称为“冒泡”,因为事件像气泡一样在文档树中上升。

将事件监听器附加到 section 将导致焦点事件从拖动的彩色框冒泡到父元素。 我们还可以利用 evt.target 属性,该属性包含触发事件的元素(其中一个框),而不是附加事件监听器的元素(<section> 元素)。

const preview = evt => {
  const color = evt.target.id;
  document.querySelector('#dragged').textContent = `Dragged ${color}`;
};

document.querySelector('section').addEventListener('dragstart', preview);

现在我们已经将许多事件监听器减少到只有一个! 使用更复杂的代码,效果会更大。 通过利用 Event 对象和冒泡,我们可以控制 JavaScript 事件并简化事件处理程序的代码。

点击事件呢?

evt.targetdragstartchange 等事件中非常有效,在这些事件中,只有少数元素可以接收焦点或更改输入。

但是,我们通常希望监听 click 事件,以便我们可以响应用户在应用程序中点击按钮。 click 事件针对文档中的任何元素触发,从大型 div 到小型 span。

让我们以我们的可拖动彩色框为例,并使它们可点击。

<section>
  <div id="red" draggable="true">
    <span>R</span>
  </div>
  <div id="yellow" draggable="true">
    <span>Y</span>
  </div>
  <div id="green" draggable="true">
    <span>G</span>
  </div>
</section>

<p id="clicked">Clicked a box</p>
const preview = evt => {
  const color = evt.target.id;
  document.querySelector('#clicked').textContent = `Clicked ${color}`;
};

document.querySelector('section').addEventListener('click', preview);

查看 CodePen 上的
点击事件:不完全有效
,作者是 Tiger Oakes (@NotWoods)
CodePen 上。

在测试此代码时,请注意,有时在点击框时,不会在“已点击”中附加任何内容。 它不起作用的原因是每个框都包含一个 <span> 元素,可以点击该元素,而不是可拖动的 <div> 元素。 由于 span 没有设置 ID,因此 evt.target.id 属性为空字符串。

我们只关心代码中的彩色框。 如果我们点击框内的某个位置,我们需要找到父框元素。 我们可以使用 element.closest() 找到最靠近点击元素的父元素。

const preview = evt => {
  const element = evt.target.closest('div[draggable]');
  if (element != null) {
    const color = element.id;
    document.querySelector('#clicked').textContent = `Clicked ${color}`;
  }
};

查看 CodePen 上的
点击事件:使用 .closest
,作者是 Tiger Oakes (@NotWoods)
CodePen 上。

现在我们可以对 click 事件使用单个监听器! 如果 element.closest() 返回 null,这意味着用户点击了彩色框之外的某个位置,我们应该忽略该事件。

更多示例

以下是一些其他示例,展示了如何利用单个事件监听器。

列表

一个常见的模式是拥有一个可以交互的项目列表,其中新项目使用 JavaScript 动态插入。 如果我们为每个项目附加了事件监听器,那么当每次生成新元素时,我们的代码都必须处理事件监听器。

<div id="buttons-container"></div>
<button id="add">Add new button</button>
let buttonCounter = 0;
document.querySelector('#add').addEventListener('click', evt => {
  const newButton = document.createElement('button');
  newButton.textContent = buttonCounter;
  
  // Make a new event listener every time "Add new button" is clicked
  newButton.addEventListener('click', evt => {

    // When clicked, log the clicked button's number.
    document.querySelector('#clicked').textContent = `Clicked button #${newButton.textContent}`;
  });

  buttonCounter++;

  const container = document.querySelector('#buttons-container');
  container.appendChild(newButton);
});

查看 CodePen 上的
列表:无冒泡
,作者是 Tiger Oakes (@NotWoods)
CodePen 上。

通过利用冒泡,我们可以在容器上有一个事件监听器。 如果我们在应用程序中创建了许多元素,这将使监听器数量从 n 减少到两个。

let buttonCounter = 0;
const container = document.querySelector('#buttons-container');
document.querySelector('#add').addEventListener('click', evt => {
  const newButton = document.createElement('button');
  newButton.dataset.number = buttonCounter;
  buttonCounter++;

  container.appendChild(newButton);
});
container.addEventListener('click', evt => {
  const clickedButton = evt.target.closest('button');
  if (clickedButton != null) {
    // When clicked, log the clicked button's number.
    document.querySelector('#clicked').textContent = `Clicked button #${clickedButton.dataset.number}`;
  }
});

表单

也许有一个包含许多输入的表单,并且我们希望将所有用户响应收集到一个对象中。

<form>
  <label>Name: <input name="name" type="text"/></label>
  <label>Email: <input name="email" type="email"/></label>
  <label>Password: <input name="password" type="password"/></label>
</form>
<p id="preview"></p>
let responses = {
  name: '',
  email: '',
  password: ''
};

document
  .querySelector('input[name="name"]')
  .addEventListener('change', evt => {
    const inputElement = document.querySelector('input[name="name"]');
    responses.name = inputElement.value;
    document.querySelector('#preview').textContent = JSON.stringify(responses);
  });
document
  .querySelector('input[name="email"]')
  .addEventListener('change', evt => {
    const inputElement = document.querySelector('input[name="email"]');
    responses.email = inputElement.value;
    document.querySelector('#preview').textContent = JSON.stringify(responses);
  });
document
  .querySelector('input[name="password"]')
  .addEventListener('change', evt => {
    const inputElement = document.querySelector('input[name="password"]');
    responses.password = inputElement.value;
    document.querySelector('#preview').textContent = JSON.stringify(responses);
  });

查看 CodePen 上的
表单:无冒泡
,作者是 Tiger Oakes (@NotWoods)
CodePen 上。

让我们切换到父 <form> 元素上的单个监听器。

let responses = {
  name: '',
  email: '',
  password: ''
};

document.querySelector('form').addEventListener('change', evt => {
  responses[evt.target.name] = evt.target.value;
  document.querySelector('#preview').textContent = JSON.stringify(responses);
});

结论

现在我们知道了如何利用事件冒泡和事件对象将复杂的事件处理程序简化为几个……有时甚至只需要一个!希望这篇文章能让你以新的视角思考事件处理程序。我知道在我早期的开发生涯中,为了完成同样的事情,我编写了重复的代码,这对我来说是一个启示。