React 允许您向 JSX 添加事件处理程序。事件处理程序是您自己的函数,将在响应单击、悬停、聚焦表单输入等交互时触发。

您将学习

  • 编写事件处理程序的不同方法
  • 如何从父组件传递事件处理逻辑
  • 事件如何传播以及如何停止事件传播

添加事件处理程序

要添加事件处理程序,您首先需要定义一个函数,然后将其作为 prop 传递给相应的 JSX 标签。例如,下面是一个按钮,它目前不做任何事情:

export default function Button() {
  return (
    <button>
      I don't do anything
    </button>
  );
}

您可以按照以下三个步骤操作,使其在用户单击时显示一条消息:

  1. 在您的 Button 组件内部声明一个名为 handleClick 的函数。
  2. 在该函数内部实现逻辑(使用 alert 显示消息)。
  3. onClick={handleClick} 添加到 <button> JSX 中。
export default function Button() {
  function handleClick() {
    alert('You clicked me!');
  }

  return (
    <button onClick={handleClick}>
      Click me
    </button>
  );
}

您定义了 handleClick 函数,然后将其作为 prop 传递<button>handleClick 是一个事件处理程序。事件处理函数:

  • 通常在组件内部定义。
  • 名称以 handle 开头,后跟事件名称。

按照惯例,通常将事件处理程序命名为 handle,后跟事件名称。您经常会看到 onClick={handleClick}onMouseEnter={handleMouseEnter} 等等。

或者,您可以在 JSX 中内联定义事件处理程序:

<button onClick={function handleClick() {
alert('You clicked me!');
}}>

或者,更简洁地使用箭头函数:

<button onClick={() => {
alert('You clicked me!');
}}>

所有这些样式都是等效的。内联事件处理程序对于短函数来说很方便。

陷阱

传递给事件处理程序的函数必须是传递的,而不是被调用的。例如:

传递函数(正确)调用函数(错误)
<button onClick={handleClick}><button onClick={handleClick()}>

区别很微妙。在第一个例子中,handleClick 函数作为 onClick 事件处理程序传递。这告诉 React 记住它,并且只在用户单击按钮时才调用您的函数。

在第二个例子中,handleClick() 末尾的 () 会在渲染期间立即触发该函数,而无需任何单击操作。这是因为JSX 中的 {} 内的 JavaScript 代码会立即执行。

当您编写内联代码时,同样的陷阱会以不同的方式出现:

传递函数(正确)调用函数(错误)
<button onClick={() => alert('...')}><button onClick={alert('...')}>

像这样传递内联代码不会在单击时触发,而是在每次组件渲染时都会触发。

// This alert fires when the component renders, not when clicked!
<button onClick={alert('You clicked me!')}>

如果要内联定义事件处理程序,请将其包装在匿名函数中,如下所示:

<button onClick={() => alert('You clicked me!')}>

这将创建一个函数,以便稍后调用,而不是在每次渲染时都执行其中的代码。

在这两种情况下,您要传递的都是一个函数:

  • <button onClick={handleClick}> 传递了 handleClick 函数。
  • <button onClick={() => alert('...')}> 传递了 () => alert('...') 函数。

阅读有关箭头函数的更多信息。

在事件处理程序中读取 props

因为事件处理程序是在组件内部声明的,所以它们可以访问组件的 props。下面是一个按钮,当单击它时,会显示一个包含其 message prop 的警告:

function AlertButton({ message, children }) {
  return (
    <button onClick={() => alert(message)}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div>
      <AlertButton message="Playing!">
        Play Movie
      </AlertButton>
      <AlertButton message="Uploading!">
        Upload Image
      </AlertButton>
    </div>
  );
}

这使得这两个按钮可以显示不同的消息。尝试更改传递给它们的消息。

将事件处理程序作为 prop 传递

通常,您希望父组件指定子组件的事件处理程序。以按钮为例:根据您使用 Button 组件的位置,您可能希望执行不同的函数 - 例如一个函数播放电影,另一个函数上传图像。

为此,请将组件从其父组件接收到的 prop 作为事件处理程序传递,如下所示

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

function PlayButton({ movieName }) {
  function handlePlayClick() {
    alert(`Playing ${movieName}!`);
  }

  return (
    <Button onClick={handlePlayClick}>
      Play "{movieName}"
    </Button>
  );
}

function UploadButton() {
  return (
    <Button onClick={() => alert('Uploading!')}>
      Upload Image
    </Button>
  );
}

export default function Toolbar() {
  return (
    <div>
      <PlayButton movieName="Kiki's Delivery Service" />
      <UploadButton />
    </div>
  );
}

此处,Toolbar 组件渲染一个 PlayButton 和一个 UploadButton

  • PlayButtonhandlePlayClick 作为 onClick prop 传递给内部的 Button
  • UploadButton() => alert('Uploading!') 作为 onClick prop 传递给内部的 Button

最后,您的 Button 组件接受一个名为 onClick 的 prop。它使用 onClick={onClick} 将该 prop 直接传递给内置的浏览器 <button>。这告诉 React 在单击时调用传递的函数。

如果您使用设计系统,则像按钮这样的组件通常包含样式但不指定行为。相反,像 PlayButtonUploadButton 这样的组件会向下传递事件处理程序。

命名事件处理程序 prop

<button><div> 这样的内置组件只支持浏览器事件名称,比如 onClick。但是,当您构建自己的组件时,您可以随意命名其事件处理程序 prop。

按照惯例,事件处理程序 prop 应以 on 开头,后跟一个大写字母。

例如,Button 组件的 onClick prop 可以称为 onSmash

function Button({ onSmash, children }) {
  return (
    <button onClick={onSmash}>
      {children}
    </button>
  );
}

export default function App() {
  return (
    <div>
      <Button onSmash={() => alert('Playing!')}>
        Play Movie
      </Button>
      <Button onSmash={() => alert('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}

在这个例子中,<button onClick={onSmash}> 表明浏览器 <button>(小写)仍然需要一个名为 onClick 的 prop,但您的自定义 Button 组件接收到的 prop 名称由您决定!

当您的组件支持多种交互时,您可以为特定于应用程序的概念命名事件处理程序 prop。例如,这个 Toolbar 组件接收 onPlayMovieonUploadImage 事件处理程序

export default function App() {
  return (
    <Toolbar
      onPlayMovie={() => alert('Playing!')}
      onUploadImage={() => alert('Uploading!')}
    />
  );
}

function Toolbar({ onPlayMovie, onUploadImage }) {
  return (
    <div>
      <Button onClick={onPlayMovie}>
        Play Movie
      </Button>
      <Button onClick={onUploadImage}>
        Upload Image
      </Button>
    </div>
  );
}

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

请注意,App 组件不需要知道 Toolbar 将如何处理 onPlayMovieonUploadImage。这是 Toolbar 的实现细节。在这里,Toolbar 将它们作为 onClick 处理程序传递给它的 Button,但它稍后也可以在键盘快捷键上触发它们。在像 onPlayMovie 这样的特定于应用程序的交互之后命名 prop,使您能够灵活地更改它们以后的使用方式。

注意

确保您对事件处理程序使用了适当的 HTML 标签。例如,要处理单击事件,请使用 <button onClick={handleClick}> 而不是 <div onClick={handleClick}>。使用真正的浏览器 <button> 可以启用内置的浏览器行为,如键盘导航。如果您不喜欢按钮的默认浏览器样式,并希望使其看起来更像链接或其他 UI 元素,则可以使用 CSS 来实现。 了解有关编写可访问标记的更多信息。

事件传播

事件处理程序还会捕获来自组件可能具有的任何子组件的事件。我们说一个事件“冒泡”或“传播”到树上:它从事件发生的地方开始,然后向上到树。

这个 <div> 包含两个按钮。<div> 每个按钮都有自己的 onClick 处理程序。您认为单击按钮时会触发哪些处理程序?

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <button onClick={() => alert('Playing!')}>
        Play Movie
      </button>
      <button onClick={() => alert('Uploading!')}>
        Upload Image
      </button>
    </div>
  );
}

如果单击任一按钮,它的 onClick 将首先运行,然后是父级 <div>onClick。所以会出现两条消息。如果单击工具栏本身,则只会运行父级 <div>onClick

陷阱

除了 onScroll 之外,所有事件都在 React 中传播,onScroll 只作用于您附加它的 JSX 标签。

阻止传播

事件处理程序接收一个 事件对象 作为其唯一参数。按照惯例,它通常被称为 e,代表“事件”。您可以使用此对象读取有关事件的信息。

该事件对象还允许您停止传播。如果您想阻止事件到达父组件,您需要调用 e.stopPropagation(),就像这个 Button 组件所做的那样

function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <Button onClick={() => alert('Playing!')}>
        Play Movie
      </Button>
      <Button onClick={() => alert('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}

当您点击按钮时

  1. React 会调用传递给 <button>onClick 处理程序。
  2. 该处理程序在 Button 中定义,执行以下操作
    • 调用 e.stopPropagation(),阻止事件进一步冒泡。
    • 调用 onClick 函数,该函数是从 Toolbar 组件传递的 props。
  3. 该函数在 Toolbar 组件中定义,显示按钮自身的警告。
  4. 由于传播已停止,因此父级 <div>onClick 处理程序*不会*运行。

由于 e.stopPropagation() 的结果,点击按钮现在只显示一个警告(来自 <button>),而不是两个(来自 <button> 和父工具栏 <div>)。点击按钮与点击周围的工具栏不同,因此对于此 UI 来说,停止传播是有意义的。

深入探讨

捕获阶段事件

在极少数情况下,您可能需要捕获子元素上的所有事件,*即使它们已停止传播*。例如,您可能希望将每次点击都记录到分析中,而不管传播逻辑如何。您可以通过在事件名称的末尾添加 Capture 来做到这一点

<div onClickCapture={() => { /* this runs first */ }}>
<button onClick={e => e.stopPropagation()} />
<button onClick={e => e.stopPropagation()} />
</div>

每个事件都分三个阶段传播

  1. 它向下传播,调用所有 onClickCapture 处理程序。
  2. 它运行被点击元素的 onClick 处理程序。
  3. 它向上传播,调用所有 onClick 处理程序。

捕获事件对于路由器或分析等代码很有用,但在应用程序代码中您可能不会使用它们。

传递处理程序作为传播的替代方案

请注意,此点击处理程序如何运行一行代码,*然后*调用父级传递的 onClick props

function Button({ onClick, children }) {
return (
<button onClick={e => {
e.stopPropagation();
onClick();
}}>
{children}
</button>
);
}

您可以在调用父级 onClick 事件处理程序之前,在此处理程序中添加更多代码。此模式提供了一种传播的*替代方案*。它允许子组件处理事件,同时也允许父组件指定一些附加行为。与传播不同,它不是自动的。但这种模式的好处是,您可以清楚地跟踪由于某些事件而执行的整个代码链。

如果您依赖传播并且难以跟踪哪些处理程序执行以及原因,请尝试使用此方法。

阻止默认行为

某些浏览器事件具有与其关联的默认行为。例如,<form> 提交事件(当点击其中的按钮时发生)默认情况下会重新加载整个页面

export default function Signup() {
  return (
    <form onSubmit={() => alert('Submitting!')}>
      <input />
      <button>Send</button>
    </form>
  );
}

您可以对事件对象调用 e.preventDefault() 以阻止这种情况发生

export default function Signup() {
  return (
    <form onSubmit={e => {
      e.preventDefault();
      alert('Submitting!');
    }}>
      <input />
      <button>Send</button>
    </form>
  );
}

不要混淆 e.stopPropagation()e.preventDefault()。它们都很有用,但彼此无关

事件处理程序可以有副作用吗?

当然可以!事件处理程序是副作用的最佳位置。

与渲染函数不同,事件处理程序不需要是 纯的,因此它是*改变*某些东西的好地方——例如,响应输入而改变输入的值,或响应按钮按下而改变列表。然而,为了改变某些信息,您首先需要一些方法来存储它。在 React 中,这是通过使用 状态(组件的内存) 来完成的。您将在下一页了解更多关于它的信息。

回顾

  • 您可以通过将函数作为 prop 传递给像 <button> 这样的元素来处理事件。
  • 事件处理程序必须被传递,而不是被调用! onClick={handleClick},而不是 onClick={handleClick()}
  • 您可以单独或内联定义事件处理函数。
  • 事件处理程序在组件内部定义,因此它们可以访问 props。
  • 您可以在父组件中声明一个事件处理程序,并将其作为 prop 传递给子组件。
  • 您可以使用特定于应用程序的名称定义您自己的事件处理程序 props。
  • 事件会向上冒泡。在第一个参数上调用 e.stopPropagation() 可以防止这种情况发生。
  • 事件可能会有不必要的默认浏览器行为。调用 e.preventDefault() 可以防止这种情况发生。
  • 从子处理程序显式调用事件处理程序 prop 是替代冒泡的好方法。

挑战 1 2:
修复事件处理程序

单击此按钮应该在白色和黑色之间切换页面背景。但是,当您单击它时没有任何反应。解决这个问题。(不要担心 handleClick 内部的逻辑——那部分没问题。)

export default function LightSwitch() {
  function handleClick() {
    let bodyStyle = document.body.style;
    if (bodyStyle.backgroundColor === 'black') {
      bodyStyle.backgroundColor = 'white';
    } else {
      bodyStyle.backgroundColor = 'black';
    }
  }

  return (
    <button onClick={handleClick()}>
      Toggle the lights
    </button>
  );
}