响应事件

React 让你可以向 JSX 中添加 事件处理函数。事件处理函数是你自己编写的函数,它们会在点击、悬停、聚焦表单输入框等交互发生时被触发。

You will learn

  • 编写事件处理函数的不同方式
  • 如何从父组件传递事件处理逻辑
  • 事件如何传播以及如何阻止传播

添加事件处理函数

要添加一个事件处理函数,你首先要定义一个函数,然后将它作为 prop 传递给对应的 JSX 标签。例如,这里有一个目前还不会做任何事情的按钮:

export default function Button() {
  return (
    <button>
      我什么都不会做
    </button>
  );
}

你可以通过以下三个步骤,让它在用户点击时显示一条消息:

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

  return (
    <button onClick={handleClick}>
      点击我
    </button>
  );
}

你定义了 handleClick 函数,然后将它作为 prop 传递给了 <button>handleClick 是一个事件处理函数。 事件处理函数通常:

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

按照惯例,通常会将事件处理函数命名为 handle 加上事件名。你经常会看到 onClick={handleClick}onMouseEnter={handleMouseEnter} 等写法。

另外,你也可以在 JSX 中直接内联定义事件处理函数:

<button onClick={function handleClick() {
alert('你点击了我!');
}}>

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

<button onClick={() => {
alert('你点击了我!');
}}>

这些写法都是等价的。对于较短的函数来说,内联事件处理函数很方便。

Pitfall

传递给事件处理函数的应该是函数本身,而不是调用它。例如:

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

区别很微妙。在第一个示例中,handleClick 函数作为 onClick 事件处理函数被传递。这样 React 就会记住它,并且只会在用户点击按钮时调用你的函数。

在第二个示例中,handleClick() 末尾的 () 会在渲染过程中立即执行函数,而不是等到点击发生。这是因为 JSX 中的 {} 里的 JavaScript 会立刻执行。

当你内联编写代码时,同样的陷阱会以另一种方式出现:

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

像这样传入内联代码并不会在点击时触发——它会在组件每次渲染时执行:

// 这个 alert 会在组件渲染时触发,而不是在点击时触发!
<button onClick={alert('你点击了我!')}>

如果你想内联定义事件处理函数,请像这样把它包在匿名函数里:

<button onClick={() => alert('你点击了我!')}>

这样不会在每次渲染时执行里面的代码,而是创建一个稍后会被调用的函数。

无论哪种情况,你要传递的都应该是一个函数:

  • <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!">
        播放电影
      </AlertButton>
      <AlertButton message="Uploading!">
        上传图片
      </AlertButton>
    </div>
  );
}

这让这两个按钮可以显示不同的消息。试着修改传给它们的消息。

将事件处理函数作为 props 传递

通常你会希望父组件指定子组件的事件处理函数。以按钮为例:根据你在什么地方使用 Button 组件,你可能希望执行不同的函数——一个播放电影,另一个上传图片。

要做到这一点,可以像这样把组件从父组件接收到的 prop 作为事件处理函数传递进去:

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

function PlayButton({ movieName }) {
  function handlePlayClick() {
    alert(`正在播放 ${movieName}!`);
  }

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

function UploadButton() {
  return (
    <Button onClick={() => alert('Uploading!')}>
      上传图片
    </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 这样的组件会向下传递事件处理函数。

为事件处理函数 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!')}>
        播放电影
      </Button>
      <Button onSmash={() => alert('Uploading!')}>
        上传图片
      </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}>
        播放电影
      </Button>
      <Button onClick={onUploadImage}>
        上传图片
      </Button>
    </div>
  );
}

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

注意 App 组件并不需要知道 Toolbar 会如何处理 onPlayMovieonUploadImage。这属于 Toolbar 的实现细节。这里,Toolbar 将它们作为 onClick 处理函数传给了它内部的 Button,但以后也可以在键盘快捷键触发时调用它们。根据应用中的具体交互来命名 prop,比如 onPlayMovie,能让你在以后灵活改变它们的使用方式。

Note

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

事件传播

事件处理函数也会捕获组件子元素触发的事件。我们说事件会在树中“冒泡”或“传播”:它从事件发生的位置开始,然后向上遍历组件树。

这个 <div> 包含两个按钮。<div> 和每个按钮都有自己的 onClick 处理函数。你觉得点击按钮时会触发哪些处理函数?

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('你点击了工具栏!');
    }}>
      <button onClick={() => alert('Playing!')}>
        播放电影
      </button>
      <button onClick={() => alert('Uploading!')}>
        上传图片
      </button>
    </div>
  );
}

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

Pitfall

在 React 中,除了 onScroll 之外,所有事件都会传播,onScroll 只在你附加它的 JSX 标签上生效。

阻止传播

事件处理函数接收一个 事件对象 作为唯一参数。按照惯例,它通常被称为 e,代表 “event”。你可以使用这个对象来读取事件信息。

这个事件对象也能让你阻止传播。如果你想阻止事件到达父组件,就需要像下面这个 Button 组件一样调用 e.stopPropagation()

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

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('你点击了工具栏!');
    }}>
      <Button onClick={() => alert('Playing!')}>
        播放电影
      </Button>
      <Button onClick={() => alert('Uploading!')}>
        上传图片
      </Button>
    </div>
  );
}

当你点击按钮时:

  1. React 调用传给 <button>onClick 处理函数。
  2. 这个在 Button 中定义的处理函数会执行以下操作:
    • 调用 e.stopPropagation(),阻止事件继续向上冒泡。
    • 调用 onClick 函数,这是从 Toolbar 组件传入的 prop。
  3. 这个在 Toolbar 组件中定义的函数会显示按钮自己的提示信息。
  4. 由于传播已被阻止,父级 <div>onClick 处理函数不会运行。

由于调用了 e.stopPropagation(),现在点击按钮时只会显示一个提示框(来自 <button>),而不是两个(来自 <button> 和父级工具栏 <div>)。点击按钮和点击周围的工具栏不是一回事,所以在这个 UI 中阻止传播是合理的。

Deep Dive

捕获阶段事件

在少数情况下,即使子元素已经阻止传播,你也可能需要捕获所有子元素上的事件。例如,也许你想把每次点击都记录到分析系统中,而不管传播逻辑如何。你可以通过在事件名末尾添加 Capture 来实现:

<div onClickCapture={() => { /* 这里先执行 */ }}>
<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>发送</button>
    </form>
  );
}

你可以在事件对象上调用 e.preventDefault() 来阻止这种行为发生:

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

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

事件处理函数可以有副作用吗?

当然可以!事件处理函数正是执行副作用的最佳位置。

与渲染函数不同,事件处理函数不需要是纯函数,所以这里非常适合去改变某些东西——例如,根据输入改变输入框的值,或者根据按钮点击改变列表。不过,要改变某些信息,你首先需要某种方式来存储它。在 React 中,这是通过使用state,组件的记忆。来实现的。你会在下一页学到它的全部内容。

Recap

  • 你可以通过将一个函数作为 prop 传递给像 <button> 这样的元素来处理事件。
  • 事件处理函数必须被传递,不能被调用! onClick={handleClick},而不是 onClick={handleClick()}
  • 你可以单独定义事件处理函数,也可以内联定义。
  • 事件处理函数定义在组件内部,因此它们可以访问 props。
  • 你可以在父组件中声明一个事件处理函数,并把它作为 prop 传给子组件。
  • 你可以使用应用特定的名称来定义自己的事件处理函数 prop。
  • 事件会向上传播。调用第一个参数上的 e.stopPropagation() 可以阻止这一点。
  • 事件可能具有不希望的浏览器默认行为。调用 e.preventDefault() 可以阻止这一点。
  • 直接从子组件处理函数中调用事件处理函数 prop,是传播之外的一个不错替代方案。

Challenge 1 of 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>
  );
}