React 让你可以向 JSX 中添加 事件处理函数。事件处理函数是你自己编写的函数,它们会在点击、悬停、聚焦表单输入框等交互发生时被触发。
You will learn
- 编写事件处理函数的不同方式
- 如何从父组件传递事件处理逻辑
- 事件如何传播以及如何阻止传播
添加事件处理函数
要添加一个事件处理函数,你首先要定义一个函数,然后将它作为 prop 传递给对应的 JSX 标签。例如,这里有一个目前还不会做任何事情的按钮:
export default function Button() { return ( <button> 我什么都不会做 </button> ); }
你可以通过以下三个步骤,让它在用户点击时显示一条消息:
- 在你的
Button组件内部声明一个名为handleClick的函数。 - 在该函数内部实现逻辑(使用
alert显示消息)。 - 将
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('你点击了我!');
}}>这些写法都是等价的。对于较短的函数来说,内联事件处理函数很方便。
在事件处理函数中读取 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:
PlayButton将handlePlayClick作为onClickprop 传给内部的Button。UploadButton将() => alert('Uploading!')作为onClickprop 传给内部的Button。
最后,你的 Button 组件接收一个名为 onClick 的 prop。它把这个 prop 直接传给浏览器内置的 <button>,使用 onClick={onClick}。这会告诉 React 在点击时调用传入的函数。
如果你使用设计系统,按钮之类的组件通常只负责样式,而不指定行为。相反,像 PlayButton 和 UploadButton 这样的组件会向下传递事件处理函数。
为事件处理函数 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 组件接收 onPlayMovie 和 onUploadImage 这两个事件处理函数:
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 会如何处理 onPlayMovie 或 onUploadImage。这属于 Toolbar 的实现细节。这里,Toolbar 将它们作为 onClick 处理函数传给了它内部的 Button,但以后也可以在键盘快捷键触发时调用它们。根据应用中的具体交互来命名 prop,比如 onPlayMovie,能让你在以后灵活改变它们的使用方式。
事件传播
事件处理函数也会捕获组件子元素触发的事件。我们说事件会在树中“冒泡”或“传播”:它从事件发生的位置开始,然后向上遍历组件树。
这个 <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。
阻止传播
事件处理函数接收一个 事件对象 作为唯一参数。按照惯例,它通常被称为 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> ); }
当你点击按钮时:
- React 调用传给
<button>的onClick处理函数。 - 这个在
Button中定义的处理函数会执行以下操作:- 调用
e.stopPropagation(),阻止事件继续向上冒泡。 - 调用
onClick函数,这是从Toolbar组件传入的 prop。
- 调用
- 这个在
Toolbar组件中定义的函数会显示按钮自己的提示信息。 - 由于传播已被阻止,父级
<div>的onClick处理函数不会运行。
由于调用了 e.stopPropagation(),现在点击按钮时只会显示一个提示框(来自 <button>),而不是两个(来自 <button> 和父级工具栏 <div>)。点击按钮和点击周围的工具栏不是一回事,所以在这个 UI 中阻止传播是合理的。
Deep Dive
在少数情况下,即使子元素已经阻止传播,你也可能需要捕获所有子元素上的事件。例如,也许你想把每次点击都记录到分析系统中,而不管传播逻辑如何。你可以通过在事件名末尾添加 Capture 来实现:
<div onClickCapture={() => { /* 这里先执行 */ }}>
<button onClick={e => e.stopPropagation()} />
<button onClick={e => e.stopPropagation()} />
</div>每个事件都会经过三个阶段:
- 它向下传播,调用所有
onClickCapture处理函数。 - 它运行被点击元素的
onClick处理函数。 - 它向上传播,调用所有
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()。它们都很有用,但彼此无关:
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> ); }