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>
  );
}

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

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

通常,您希望父组件指定子组件的事件处理程序。考虑按钮:根据您使用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。它将该 prop 直接传递给内置的浏览器<button>,并使用onClick={onClick}。这告诉 React 在点击时调用传递的函数。

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

命名事件处理程序 props

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

按照约定,事件处理程序 props 应以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 名称由您决定!

当您的组件支持多种交互时,您可以为特定于应用程序的概念命名事件处理程序 props。例如,此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)命名 props 使您可以灵活地更改以后如何使用它们。

注意

确保您为事件处理程序使用适当的 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 处理程序会运行。

陷阱

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 组件传递的 prop。
  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 prop。

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>
  );
}