使用Effect同步
一些组件需要与外部系统同步。 例如,您可能希望根据 React 状态控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。 Effects 让你在渲染后运行一些代码,这样你就可以将你的组件与 React 之外的一些系统同步。
你将学习
- 什么是Effect
-Effect与事件有何不同
-如何在组件中声明 Effect
-如何跳过不必要的重新运行效果
-为什么 Effects 在开发中运行两次以及如何修复它们
什么是效果,它们与事件有何不同?
在获得效果之前,您需要熟悉两种类型的逻辑内部React组件:
- 渲染代码(在描述UI中引入)生活在组件的最高层。 这是您采取props并声明,转换它们并返回要在屏幕上看到的JSX的地方。 渲染代码必须纯净。 像数学公式一样,它只能计算结果,但不做其他事情。
- 事件处理程序(在添加互动性中引入)是您的组件内的嵌套功能,而不仅仅是计算它们。 事件处理程序可能会更新输入字段,提交HTTP POST请求以购买产品,或将用户导航到另一个屏幕。 事件处理程序包含“副作用”(它们更改程序的状态),并由特定的用户操作(例如,单击按钮或键入)引起。
有时这还不够。 考虑一个聊天室组件,该组件在屏幕上可见时必须连接到聊天服务器。 连接到服务器不是纯计算(它是副作用),因此在渲染过程中不会发生。 但是,没有单个特定事件,例如单击会导致聊天室显示。
效果让您指定由渲染本身而不是特定事件引起的副作用。 在聊天中发送消息是一个事件,因为它是由用户单击特定按钮直接引起的。 但是,设置服务器连接是一种效果,因为无论哪种交互导致组件出现,都需要发生。 效果在屏幕更新后在渲染过程的末尾运行。 这是将React组件与某些外部系统(例如网络或第三方库)同步的好时机。
注意
在此及以后,在本文中,大写的“效果”是指上述特定于反应的定义,即由渲染引起的副作用。 为了提及更广泛的编程概念,我们将说“副作用”。
您可能不需要效果
不要急于为组件添加效果。 请记住,效果通常用于“走出”您的react代码,并与某些外部系统同步。 这包括浏览器API,第三方小部件,网络等。 如果您的效果仅根据其他状态调整某些状态,则可能不需要效果。
如何写效果
要写下效果,请按照以下三个步骤:
- 声明效果。 默认情况下,您的效果将在每次渲染后运行。
- 指定效果依赖性。 大多数效果只能在需要时重新运行,而不是在每次渲染之后重新运行。 例如,只有在出现组件时才能触发淡出动画。 只有在组件出现并消失或聊天室更改时,才能连接并断开聊天室。 您将通过指定依赖项来学习如何控制它。
- 如果需要,请添加清理。 一些效果需要指定如何停止,撤消或清理他们正在做的任何事情。 例如,“连接”需要“断开连接”,“订阅”需要“取消订阅”和“获取”需要“取消”或“忽略”。 您将通过返回清理功能来学习如何做到这一点。
让我们详细了解每个步骤。
步骤1:声明效果
要声明组件中的效果,请从React导入使用效果挂钩:
import { useEffect } from 'react';
然后,将其称为组件的最高级别,并在您的效果中放置一些代码:
function MyComponent() {
useEffect(() => {
// Code here will run after *every* render
});
return <div />;
}
每次您的组件渲染时,React都会更新屏幕,然后运行内部使用效果的代码。 换句话说,使用效果“延迟”一块代码从运行到该渲染在屏幕上反映。
让我们看看如何使用效果与外部系统同步。 考虑<videoplayer> React组件。 通过传递Isplay prop来控制它是在播放还是暂停它是很高兴的:
<VideoPlayer isPlaying={isPlaying} />;
您的自定义录像机组件呈现内置浏览器<video>标签:
function VideoPlayer({ src, isPlaying }) {
// TODO: do something with isPlaying
return <video src={src} />;
}
但是,浏览器<video>标签没有Inplaying Prop。 控制它的唯一方法是在DOM元素上手动调用play()和pape()方法。 您需要同步iSPlaying Prop的价值,该道具告诉视频当前是否应该在播放,诸如play()和pape()之类的命令。
我们需要先获取<video> dom节点的参考。
您可能很想尝试在渲染过程中调用play()或pape(),但这是不正确的:
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
if (isPlaying) {
ref.current.play(); // Calling these while rendering isn't allowed.
} else {
ref.current.pause(); // Also, this crashes.
}
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
return (
<>
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
此代码不正确的原因是它在渲染过程中尝试使用DOM节点做某事。 在React中,渲染应为JSX的纯计算,并且不应包含诸如修改DOM之类的副作用。
此外,当第一次调用VideoPlayer时,它的DOM尚不存在! 尚无dom节点可以调用play()或pape()on,因为在返回JSX之后,React不知道要创建什么DOM。
这里的解决方案是将副作用与使用效应相结合,以将其从渲染计算中移出:
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 ref={ref} src={src} loop playsInline />;
}
通过将DOM更新包裹在效果上,您让React首先更新屏幕。 然后您的效果运行。
当您的VideoPlayer组件渲染(第一次或重新租赁)时,会发生一些事情。 首先,React将更新屏幕,以确保<video>标签在DOM中具有正确的道具。 然后,React将运行您的效果。 最后,您的效果将根据iSPlaying Prop的价值调用Play()或暂停()。
多次按Play/Pause,看看视频播放器如何与Isplay值同步:
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
return (
<>
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
在此示例中,您同步反应状态的“外部系统”是浏览器媒体API。 您可以使用类似的方法将遗产非react代码(例如jQuery插件)包含在声明的React组件中。
请注意,在实践中控制视频播放器要复杂得多。 呼叫play()可能会失败,用户可能会使用内置浏览器控件播放或暂停,依此类推。 这个示例非常简化和不完整。
陷阱
默认情况下,效果在每次渲染后运行。 这就是为什么这样的代码会产生无限循环:
const [count, setCount] = useState(0); useEffect(() => { setCount(count + 1); });
效果是由于渲染而产生的。 设置状态触发器渲染。 立即设置状态就像将电源插座插入自身一样。 效果运行,它设置了状态,这会导致重新渲染,从而导致效果运行,它再次设置状态,这会导致另一个重新渲染等。
效果通常应将组件与外部系统同步。 如果没有外部系统,您只想根据其他状态调整某些状态,则可能不需要效果。
步骤2:指定效果依赖性
默认情况下,效果在每次渲染后运行。 通常,这不是您想要的:
- 有时,这很慢。 与外部系统同步并不总是即时的,因此您可能需要跳过执行此操作,除非有必要。 例如,您不想在每次击键上重新连接到聊天服务器。
- 有时候,这是错误的。 例如,您不想在每次击键上触发组件淡出动画。 当组件首次出现时,动画只能播放一次。
为了说明问题,这是上一个带有几个控制台的示例。log调用和更新父组件状态的文本输入。 注意键入如何导致效果重新运行:
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
console.log('Calling video.play()');
ref.current.play();
} else {
console.log('Calling video.pause()');
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
您可以通过将依赖项数组指定为使用效率调用的第二个参数,从而跳过不必要地重新运行效果。 首先在第14行上的上面示例中添加一个空[]数组:
useEffect(() => {
// ...
}, []);
您应该看到一个错误,说React Hook Useffect缺少依赖性:“ iSplaying”:
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
console.log('Calling video.play()');
ref.current.play();
} else {
console.log('Calling video.pause()');
ref.current.pause();
}
}, []); // This causes an error
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
问题在于,效果内部的代码取决于效果的属性来决定要做什么,但是这种依赖性并未明确声明。 要解决此问题,请添加iSplay将依赖项数组添加:
useEffect(() => {
if (isPlaying) { // It's used here...
// ...
} else {
// ...
}
}, [isPlaying]); // ...so it must be declared here!
现在声明了所有依赖项,因此没有错误。 指定[isPlaying],因为依赖性阵列告诉React,如果isPlaying与以前的渲染过程相同,则应跳过您的效果。 随着此更改,输入输入不会导致重新运行效果,但是按Play/暂停确实可以。
依赖项数组可以包含多个依赖项。 如果您指定的所有依赖项的值与上一个渲染期间的值完全相同,则React只会跳过重新运行效果。 React使用对象比较比较依赖关系值。 有关更多详细信息,请参见使用效果API参考。
请注意,您无法“选择”您的依赖项。 如果您指定的依赖项与基于效果内部的代码的预期不匹配,则您将遇到lint错误。 这有助于捕获代码中的许多错误。 如果您的效果使用一些值,但您不想在更改时重新运行效果,则需要编辑效果代码本身,以免“需要”该依赖性。
缺陷
没有依赖性数组和空的[]依赖关系阵列的行为非常不同:
useEffect(() => { // This runs after every render }); useEffect(() => { // This runs only on mount (when the component appears) }, []); useEffect(() => { // This runs on mount *and also* if either a or b have changed since the last render }, [a, b]);
我们将仔细研究“安装”在下一步中的含义。
深度阅读:为什么从依赖性数组中省略了ref?
这种效果同时使用Ref和isPlaying,但仅将isPlaying声明为依赖性:
function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } }, [isPlaying]);
这是因为ref对象具有稳定的身份:React保证您将始终从每个渲染上的同一useref调用中获取相同的对象。 它永远不会改变,因此它本身永远不会导致重新运行的效果。 因此,无论您是否包括它都没关系。 包括它也很好。
useState返回的set函数也具有稳定的身份,因此您经常会看到它们也从依赖项中省略了。 如果Linter允许您省略没有错误的依赖性,则可以安全地进行。省略始终稳定的依赖性只有在Linter可以“看到”对象稳定时起作用。 例如,如果从父组件传递ref,则必须在依赖项数组中指定它。 但是,这很好,因为您不知道父部件是否总是通过相同的ref,或者有条件地通过了几个ref之一。 因此,您的效果取决于ref的传递。
步骤3:如果需要,请添加清理
考虑一个不同的示例。 您正在编写一个聊天室组件,该组件出现时需要连接到聊天服务器。 给您一个createConnection()API,该API返回使用Connect()和Disconnect()方法的对象。 在将组件显示给用户时如何保持连接?
首先编写效果逻辑:
useEffect(() => {
const connection = createConnection();
connection.connect();
});
每次重新渲染后连接到聊天的速度会很慢,因此您添加依赖项数组:
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
效果内部的代码不使用任何属性或状态,因此您的依赖性数组为[](空)。 这告诉反应仅在组件“安装”时,即首次出现在屏幕上时。
让我们尝试运行此代码:
chat.js
export function createConnection() {
// A real implementation would actually connect to the server
return {
connect() {
console.log('✅ Connecting...');
},
disconnect() {
console.log('❌ Disconnected.');
}
};
}
App.js
import { useEffect } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
return <h1>Welcome to the chat!</h1>;
}
此效果仅在mount时运行,因此您可能会期望在控制台中打印一次“连接...”。 但是,如果检查控制台,“✅连接...”将两次打印。 为什么会发生?
想象一下,聊天室组件是具有许多不同屏幕的较大应用程序的一部分。 用户在聊天页面上开始旅程。 组件安装和调用Connection.connect()。 然后,想象用户将导航到另一个屏幕,例如,到“设置”页面。 聊天室组件卸下。 最后,用户点击回去并再次安装聊天室。 这将建立第二个连接,但是第一个连接从未被破坏! 当用户浏览应用程序时,连接将不断堆积。
如果没有大量的手动测试,就很容易错过这样的错误。 为了帮助您快速发现它们,开发中的react重新安装了每个组件在初始安装后立即进行一次重新安装。 看到“✅连接...”日志两次,可帮助您注意实际问题:当组件卸载时,您的代码不会关闭连接。
要解决该问题,请从您的效果中返回清理功能:
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);
React每次都会在效果再次运行之前调用您的清理功能,以及组件卸下(删除)时的最后一次。 让我们看看实现清理功能时会发生什么
现在,您可以在开发中获得三个控制台日志:
- "✅ Connecting..."
- "❌ Disconnected."
- "✅ Connecting..."
这是开发模式中的正确行为。 通过重新启动组件,React验证导航和向后导航不会破坏您的代码。 断开并再次连接正是应该发生的事情! 当您很好地实施清理时,一旦运行效果,将其运行,将其清理并再次运行,则不应有可见的差异。 有一个额外的连接/断开通话对,因为React正在探索您的代码中的错误。 这是正常的,您不应该试图使它消失。
在生产中,您只会看到一次打印一次“✅连接...”。 重新安装组件仅在开发中发生,以帮助您找到需要清理的效果。 您可以关闭严格的模式以选择退出开发行为,但我们建议保持它。 这使您可以找到许多像上面的错误。
如何处理开发两次发射的效果?
React有意重新安装您的开发组件,以帮助您在上一个示例中找到错误。 正确的问题不是“如何运行效果一次”,而是“如何修复我的效果,以便在重新安装后起作用”。
通常,答案是实施清理功能。 清理功能应停止或撤消所做的任何效果。 经验法则是,用户不能区分运行一次(如生产)和设置→清理→设置顺序(如开发中所见)。
您写的大多数效果都适合下面的常见模式之一。
控制非react小部件
有时,您需要添加未编写反应的UI小部件。 例如,假设您正在将地图组件添加到页面中。 它具有setZoomlevel()方法,您希望将缩放级别与React代码中的Zoomlevel状态变量保持同步。 您的效果看起来与此相似:
useEffect(() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
}, [zoomLevel]);
请注意,在这种情况下,不需要清理。 在开发中,React将两次调用效果,但这不是问题,因为用相同值两次调用SetZoomlevel不会做任何事情。 它可能会稍慢一些,但这并不重要,因为重新安装是仅开发时发生,并且不会在生产中发生。
某些API可能不允许您连续两次调用。 例如,如果您调用两次,则内置< dialog >元素的showModal方法将抛出。 实施清理功能并使其关闭对话框:
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);
在开发中,您的效果将调用ShowModal(),然后立即close(),然后再次Showmodal()。 与您在生产中看到的那样,它具有与调用ShowModal()相同的用户可见行为。
订阅事件
如果您的效果订阅了某些东西,则清理功能应退订:
useEffect(() => {
function handleScroll(e) {
console.log(e.clientX, e.clientY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
在开发中,您的效果将调用AddEventListener(),然后立即removeEventListener(),然后使用同一处理程序再次使用AddeventListener()。 因此,一次只会有一个主动订阅。 与您在生产中看到的那样,这与调用AddeventListener()的用户可见行为相同。
触发动画
如果您的效果使某些内容动画,则清理功能应将动画重置为初始值:
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // Trigger the animation
return () => {
node.style.opacity = 0; // Reset to the initial value
};
}, []);
在开发中,不透明度将设置为1,然后将其设置为0,然后再次设置为1。 这应该具有与直接设置为1的用户可见行为相同的行为,这就是生产中发生的情况。 如果您使用第三方动画库并支持补间,则清理功能应将补间的时间表重置为初始状态。
获取数据
如果您的效果获取了一些东西,则清理功能应中止提取或忽略其结果:
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
您不能“撤消”已经发生的网络请求,但是您的清理功能应确保不再相关的获取不会不断影响您的应用程序。 例如,如果userId从“ Alice”更改为“ Bob”,则清理可确保即使在“ Bob”之后到达“ Alice”的响应也被忽略。
在开发中,您将在“网络”选项卡中看到两个获取。 没有什么不妥。 使用上面的方法,第一个效果将立即清理,因此其ignore变量的副本将设置为true。 因此,即使有额外的请求,由于(!ignore)检查,它也不会影响状态。
在生产中,将只有一个请求。 如果开发中的第二个请求困扰着您,最好的方法是使用一种解决请求的解决方案,并在组件之间缓存其响应:
function TodoList() {
const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);
// ...
这不仅会改善开发体验,而且还可以使您的应用程序更快。 例如,按下返回按钮的用户不必等待一些数据再次加载,因为它将被缓存。 您可以自己构建这样的缓存,也可以使用许多现有替代方案之一来手动获取效果。
深度阅读:比在Effect中获取数据更好的获取数据的替代方案是什么?
在effect中编写fetch调用,是获取数据的流行方式,尤其是在完全客户端的应用程序中。 但是,这是一种非常手动的方法,它具有重要的缺点:
- 效果不会在服务器上运行。 这意味着初始服务器渲染的HTML仅包含没有数据的加载状态。 客户端计算机将必须下载所有JavaScript并渲染您的应用程序,只是发现现在需要加载数据。 这不是很有效。
- 直接获取效果使创建“网络瀑布”变得容易。 您渲染父组件,获取一些数据,呈现子组件,然后开始获取数据。 如果网络不是很快,则比并行获取所有数据要慢得多。
- 直接获取效果通常意味着您不会预紧或缓存数据。 例如,如果组件卸载然后再次安装,则必须再次获取数据。
- 这不是符合人体工程学的。 当编写“获取电话”以不受种族条件之类的错误的方式编写fetch呼叫时,涉及很多样板代码。
此弊端列表并非特定于React。 它适用于使用在mount获取数据的任何库。 与路由一样,数据获取并不是很重要,因此我们建议采用以下方法:
- 如果使用框架,请使用其内置数据获取机制。 现代React框架具有有效且不会遭受上述陷阱的集成数据提取机制。
- 否则,请考虑使用或构建客户端缓存。 流行的开源解决方案包括React Query,useWRR和React Router 6.4+。 您也可以构建自己的解决方案,在这种情况下,您将在引擎盖下使用效果,但还可以添加逻辑以重复数据删除请求,缓存响应以及避免网络瀑布(通过预加载数据或将数据要求提高到路线上)。
如果这些方法都不适合您,则可以继续直接在效果中获取数据。
发送分析
考虑此代码,该代码在页面访问中发送分析事件:
useEffect(() => {
logVisit(url); // Sends a POST request
}, [url]);
在开发中,logVisit将针对每个URL进行两次调用,因此您可能会试图尝试解决它。 我们建议保留此代码。 像较早的示例一样,运行一次并运行两次之间没有用户可见的行为差异。 从实际的角度来看,LogVisit不应在开发中做任何事情,因为您不希望开发机器的原木偏向生产指标。 每次保存文件时,您的组件都会重新安装,因此无论如何它都会在开发过程中发送额外的访问。
在生产中,将没有重复的访问日志。
为了调试您发送的分析事件,您可以将应用程序部署到灰度环境(以生产模式运行),或者暂时退出严格的模式及其仅开发模式。 您也可以从路由更改事件处理程序而不是效果发送分析。 对于更精确的分析,相交观察者可以帮助跟踪视频中哪些组件以及它们保持多长时间。
不是效果:初始化应用程序
应用程序启动时只能运行一次。 您可以将其放在组件外面:
if (typeof window !== 'undefined') { // Check if we're running in the browser.
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}
这可以确保此类逻辑仅在浏览器加载页面后运行一次。
不是效果:购买产品
有时,即使您编写了清理功能,也无法防止两次运行效果的用户可见后果。 例如,也许您的效果发送了帖子请求,例如购买产品:
useEffect(() => {
// 🔴 Wrong: This Effect fires twice in development, exposing a problem in the code.
fetch('/api/buy', { method: 'POST' });
}, []);
您不想两次购买产品。 但是,这也是为什么您不应该将此逻辑效果效果。 如果用户转到另一个页面然后按回去怎么办? 您的效果将再次运行。 用户访问页面时,您不想购买该产品; 当用户单击“购买”按钮时,您想购买它。
购买不是由渲染引起的; 这是由特定互动引起的。 它只能运行一次,因为交互(单击)一次。 删除效果并将您的 /api /购买请求移至“购买按钮事件处理程序:
function handleClick() {
// ✅ Buying is an event because it is caused by a particular interaction.
fetch('/api/buy', { method: 'POST' });
}
这说明,如果重新安装打破了您的应用程序的逻辑,则通常会发现现有的错误。 从用户的角度来看,访问页面应该与访问它没有什么不同,单击链接,然后后退。 React验证了您的组件不会通过重新启动一次开发来打破此原理。
将所有这些放在一起
这个游乐场可以帮助您“感觉到”效果在实践中的工作方式。
本示例使用SetTimeout安排带有输入文本的控制台日志,以在效果运行后三秒钟出现。 清理功能取消了待处理的超时。 开始于按“安装组件”:
import { useState, useEffect } from 'react';
function Playground() {
const [text, setText] = useState('a');
useEffect(() => {
function onTimeout() {
console.log('⏰ ' + text);
}
console.log('🔵 Schedule "' + text + '" log');
const timeoutId = setTimeout(onTimeout, 3000);
return () => {
console.log('🟡 Cancel "' + text + '" log');
clearTimeout(timeoutId);
};
}, [text]);
return (
<>
<label>
What to log:{' '}
<input
value={text}
onChange={e => setText(e.target.value)}
/>
</label>
<h1>{text}</h1>
</>
);
}
export default function App() {
const [show, setShow] = useState(false);
return (
<>
<button onClick={() => setShow(!show)}>
{show ? 'Unmount' : 'Mount'} the component
</button>
{show && <hr />}
{show && <Playground />}
</>
);
}
首先,您将看到三个日志:调度“ a日志,取消”“日志,然后调度”“日志”。 三秒钟后,还会有一个日志。 正如您在此页面上早些时候了解到的那样,额外的时间表/取消对是因为React在开发中重新安装了该组件,以验证您已经很好地实施了清理。
现在编辑输入以说ABC。 如果您做得足够快,您将立即看到时间表“ AB”日志,然后是取消“ AB”日志和计划“ ABC”日志。 在下一个渲染效果之前,React总是清除上一个渲染的效果。 这就是为什么即使您快速键入输入,最多都会安排一次超时。 编辑几次输入并观看控制台,以感觉如何清理效果。
在输入中键入某些内容,然后立即按“卸载组件”。 请注意,卸载如何清理最后的渲染效果。 在此示例中,它清除了最后一个超时,然后才有机会开火。
最后,编辑上面的组件并评论清理功能,以使超时不会被取消。 尝试快速键入ABCDE。 您期望在三秒钟内发生什么? console.log(文本)会在超时打印最新文本并生成五个ABCDE日志吗? 尝试检查您的直觉!
三秒钟后,您应该看到一系列日志(A,AB,ABC,ABCD和ABCDE),而不是五个ABCDE日志。 每个效果都会从其相应的渲染中“捕获”文本值。 文本状态改变并不重要:渲染带有文本='ab'的效果总是会看到'ab'。 换句话说,每个渲染的效果彼此隔离。 如果您很好奇它的工作原理,则可以阅读有关闭包的材料。
深度阅读:每个渲染都有自己的效果
您可以将使用效应视为将一种行为“附加”到渲染输出上。 考虑此效果:
export default function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(roomId); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to {roomId}!</h1>; }
让我们看看用户围绕应用程序导航时到底会发生什么。
初始渲染
用户访问<ChatRoom roomId="general" />。 让我们在心理上用“general”代替roomId:
// JSX for the first render (roomId = "general") return <h1>Welcome to general!</h1>;
效果也是渲染输出的一部分。 第一个渲染的效果变成:
// Effect for the first render (roomId = "general") () => { const connection = createConnection('general'); connection.connect(); return () => connection.disconnect(); }, // Dependencies for the first render (roomId = "general") ['general']
React运行了此效果,该效果连接到“ general”聊天室。
重新渲染具有相同的依赖性
假设<ChatRoom roomId="general" />重新渲染。 JSX输出相同:
// JSX for the second render (roomId = "general") return <h1>Welcome to general!</h1>;
React认为渲染输出没有更改,因此不会更新DOM。
第二渲染的效果看起来像这样:
// Effect for the second render (roomId = "general") () => { const connection = createConnection('general'); connection.connect(); return () => connection.disconnect(); }, // Dependencies for the second render (roomId = "general") ['general']
React与第一个渲染的第二渲染与['general']的['general']进行了比较。 因为所有依赖性都是相同的,所以react忽略了第二渲染的效果。 它永远不会被打掉用。
重新渲染不同的依赖性
然后,用户访问<ChatRoom roomId="travel" />。 这次,组件返回不同的JSX:
// JSX for the third render (roomId = "travel") return <h1>Welcome to travel!</h1>;
React更新DOM,将“Welcome to general”Welcome to travel”。
第三渲染的效果看起来像这样:
// Effect for the third render (roomId = "travel") () => { const connection = createConnection('travel'); connection.connect(); return () => connection.disconnect(); }, // Dependencies for the third render (roomId = "travel") ['travel']
React从第三次渲染与第二次渲染的['general']进行比较。 一个依赖是不同的:object.is('travel','general')是false。 效果无法跳过。
在React可以应用第三渲染的效果之前,它需要清理确实运行的最后效果。 第二个渲染的效果被跳过,因此需要清理第一个渲染的效果。 如果您滚动到第一个渲染,则会发现其清理调用disconnect()在使用CreateConnection('general')创建的连接上。 这使该应用程序与“general”聊天室断开连接。
之后,React运行了第三个渲染的效果。 它连接到“travel”聊天室。
卸载
最后,假设用户会导航,而聊天室组件会卸下。 React运行最后一个效果的清理功能。 最后的效果来自第三次渲染。 第三个渲染的清理破坏了创建连接(“travel”)连接。 因此,该应用程序与“travel”室断开连接。
开发模式行为
当严格的模式打开时,对安装后的每个组件进行react重新安装(保留状态和DOM)。 这可以帮助您找到需要清理的效果,并尽早揭露诸如条件抢跑之类的错误。 此外,每当您将文件保存在开发中时,React都会重新安装效果。 这两种行为都是仅限开发模式。
回顾
- 与事件不同,效果是由渲染本身而不是特定相互作用引起的。
- 效果让您将组件与某些外部系统(第三方API,网络等)同步。
- 默认情况下,效果在每个渲染(包括初始渲染)之后运行。
- 如果其所有依赖性都具有与最后渲染期间相同的值,则React将跳过效果。
- 您不能“选择”您的依赖性。 它们由效果内部的代码确定。
- 一个空的依赖关系数组([])对应于组件“安装”,即添加到屏幕中。
- 当严格的模式开启时,React安装组件两次(仅在开发中!)以强调您的效果。
- 如果您的效果因重新安装而破裂,则需要实现清理功能。
- React将在下次效果运行之前和卸载期间调用您的清理功能。
网友评论