第四章 - 脱围机制
使用 Effect 同步
有些组件需要与外部系统同步。例如,你可能希望根据 React state 控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。Effects 会在渲染后运行一些代码,以便可以将组件与 React 之外的某些系统同步。
什么时 Effect,它与事件 (event) 有何不同?
在谈到 Effect 之前,你需要熟悉 React 组件中的两种逻辑类型:
- 渲染逻辑代码(在 描述 UI 中有介绍)位于组件的顶层。你将在这里接收 props 和 state,并对它们进行转换,最终返回你想在屏幕上看到的 JSX。渲染的代码必须是纯粹的——就像数学公式一样,它只应该“计算”结果,而不做其他任何事情。
- 事件处理程序(在 添加交互性 中介绍)是嵌套在组件内部的函数,而不仅仅是计算函数。事件处理程序可能会更新输入字段、提交 HTTP POST 请求以购买产品,或者将用户导航到另一个屏幕。事件处理程序包含由特定用户操作(例如按钮点击或键入)引起的“副作用”(它们改变了程序的状态)。
有时这还不够。考虑一个 ChatRoom
组件,它在屏幕上可见时必须连接到聊天服务器。连接到服务器不是一个纯计算(它包含副作用),因此它不能在渲染过程中发生。然而,并没有一个特定的事件(比如点击)导致 ChatRoom
被显示。
Effect允许你指定由渲染本身,而不是特定事件引起的副作用。在聊天中发送消息是一个事件,因为它直接由用户点击特定按钮引起。然而,建立服务器连接是Effect,因为它应该发生无论哪种交互导致组件出现。Effect在屏幕更新后的提交阶段运行。 这是一个很好的时机,可以将React组件与某个外部系统 (如网络或第三方库) 同步。
你可能不需要Effect
不要随意在你的组件中使用Effect。记住,Effect通常用于暂时跳出React代码并与一些外部系统进行同步。这包括浏览器API,第三方小部件,以及网络等等。如果你想用Effect仅根据其他状态调整某些状态,那么你可能不需要Effect。
如何编写 Effect
编写Effect 需要遵顼以下三个规则:
- 声明Effect。默认情况下,Effect会在每次commit后都会执行。
- 指定Effect依赖。大多数Effect应该按需执行,而不是在每次渲染后都执行。例如,,淡入动画应该只在组件出现时触发。连接和断开服务器的操作只应在组件出现和消失时,或者切换聊天室时执行。文章将介绍如何通过指定依赖来控制如何按需执行。
- 必要时添加清理(cleanup)函数。有时 Effect 需要指定如何停止,撤销,或者清除它的效果。例如,“连接"操作需要“断连”,“订阅”需要“退订”,“获取”既需要“取消”也需要“忽略”。你将学习如何使用 清理函数 来做到这一切。
第一步:声明 Effect
首先在React中引入 useEffect Hook;然后在组件顶部调用它,并传入每次渲染时都需要执行的代码
import {useEffect} from 'react';
function MyComponent () {
useEffect(() => {
// 每次渲染后都会执行此处代码
});
return <div></div>
}
当你的组件渲染时,React将更新屏幕,然后运行 useEffect 中的代码。换句话说,useEffect 会把这段代码放到屏幕更新渲染之后执行。
让我们看看如何使用 Effect 与外部系统同步。考虑一个 React组件。通过传递布尔类型的 isPlaying
prop 以控制是播放还是暂停:
<VideoPlayer isPlaying={isPlaying} />;
自定义的 组件渲染了内置的 标签:
function VideoPlayer({ src, isPlaying }) {
// TODO:使用 isPlaying 做一些事情
return <video src={src} />;
}
但是,浏览器的 标签没有 isPlaying 属性。控制它的唯一方式是在 DOM元素上调用 play() 和 pause() 方法。因此,你需要将 isPlaying prop的值 与 paly() 和 pause() 等函数的调用同步进行,该属性用于告知当前视频是否应该播放。
首先要获取 <video>
DOM 节点的 对象引用。
你可能会尝试在渲染期间调用 play()
或 pause()
,但这种做法是错的:
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
if (isPlaying) {
ref.current.play(); // 渲染期间不能调用 `play()`。
} else {
ref.current.pause(); // 同样,调用 `pause()` 也不行。
}
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
return (
<>
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? '暂停' : '播放'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
这段代码之所以 不正确,是因为它试图在渲染期间对DOM节点进行操作。在React中,JSX的渲染必须是纯粹操作,不应该包含任何像修改DOM的副作用。
而且,当第一次调用 VideoPlayer
时,对应的 DOM 节点甚至还不存在!如果连 DOM 节点都没有,那么如何调用 play()
或 pause()
方法呢!在返回 JSX 之前,React 不知道要创建什么 DOM。
解决办法是 使用 useEffect
包裹副作用,把它分离到渲染逻辑的计算过程之外:
import { useEffect, useRef } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video