作者选择Creative Commons接受捐赠,作为Write for DOnations计划的一部分。
介绍
作为JavaScript Web 开发人员,异步代码使您能够运行代码的某些部分,而其他部分仍在等待数据或解析。这意味着您的应用程序的重要部分在呈现之前不必等待不太重要的部分。使用异步代码,您还可以通过请求和显示新信息来更新您的应用程序,即使在后台处理长函数和请求时,也可以为用户提供流畅的体验。
在React开发中,异步编程提出了独特的问题。例如,当您使用 React函数式组件时,异步函数可以创建无限循环。当一个组件加载时,它可以启动一个异步函数,当异步函数解析时,它可以触发重新渲染,这将导致组件调用异步函数。本教程将解释如何使用一个名为useEffect
的特殊Hook来避免这种情况,它只会在特定数据更改时运行函数。这将让您有意识地运行异步代码,而不是在每个渲染周期上运行。
异步代码不仅限于对新数据的请求。React 有一个内置系统用于延迟加载组件,或者仅在用户需要时加载它们。当与Create React App 中的默认webpack配置结合使用时,您可以拆分代码,将大型应用程序减少为可以根据需要加载的较小部分。React 有一个特殊的组件,它会在浏览器加载新组件时显示占位符。在 React 的未来版本中,您将能够使用在嵌套组件中加载数据而不会阻塞渲染。Suspense
Suspense
在本教程中,您将通过创建一个应用程序来处理 React 中的异步数据,该应用程序显示有关河流的信息并使用setTimeout
. 在本教程结束时,您将能够使用useEffect
Hook加载异步数据。如果组件在数据解析之前卸载,您还可以安全地更新页面而不会产生错误。最后,您将使用代码拆分将大型应用程序拆分为较小的部分。
先决条件
-
你需要一个运行Node.js的开发环境;本教程在 Node.js 版本 10.20.1 和 npm 版本 6.14.4 上进行了测试。要在 macOS 或 Ubuntu 18.04 上安装它,请按照如何在 macOS 上安装 Node.js 和创建本地开发环境或如何在 Ubuntu 18.04 上安装 Node.js 的使用 PPA 安装部分中的步骤进行操作。
-
使用Create React App设置的 React 开发环境,删除了非必要的样板。要进行设置,请按照如何管理 React 类组件上的状态教程的步骤 1 — 创建空项目进行操作。本教程将
async-tutorial
用作项目名称。 -
您将使用 React 事件和 Hooks,包括
useState
和useReducer
Hooks。您可以在我们的如何使用 React 处理 DOM 和窗口事件教程以及如何使用 React 组件上的钩子管理状态的钩子中了解事件。 -
您还需要具备 JavaScript 和 HTML 的基本知识,您可以在我们的如何使用 HTML 构建网站系列和如何在 JavaScript 中编码中找到这些知识。CSS 的基本知识也很有用,您可以在Mozilla 开发人员网络找到这些知识。
第 1 步 – 加载异步数据 useEffect
在这一步中,您将使用useEffect
Hook 将异步数据加载到示例应用程序中。您将使用 Hook 来防止不必要的数据获取,在加载数据时添加占位符,并在数据解析时更新组件。在此步骤结束时,您将能够useEffect
在useState
Hook 解析时使用Hook加载数据和设置数据。
为了探索该主题,您将创建一个应用程序来显示有关世界上最长河流的信息。您将使用模拟对外部数据源的请求的异步函数加载数据。
首先,创建一个名为RiverInformation
. 制作目录:
- mkdir src/components/RiverInformation
RiverInformation.js
在文本编辑器中打开:
- nano src/components/RiverInformation/RiverInformation.js
然后添加一些占位符内容:
import React from 'react';
export default function RiverInformation() {
return(
<div>
<h2>River Information</h2>
</div>
)
}
保存并关闭文件。现在您需要将新组件导入并渲染到您的根组件。打开App.js
:
- nano src/components/App/App.js
通过添加突出显示的代码来导入和渲染组件:
import React from 'react';
import './App.css';
import RiverInformation from '../RiverInformation/RiverInformation';
function App() {
return (
<div className="wrapper">
<h1>World's Longest Rivers</h1>
<RiverInformation />
</div>
);
}
export default App;
保存并关闭文件。
最后,为了让应用更易于阅读,添加一些样式。打开App.css
:
- nano src/components/App/App.css
wrapper
通过将 CSS 替换为以下内容,向类添加一些填充:
.wrapper {
padding: 20px
}
保存并关闭文件。当您这样做时,浏览器将刷新并呈现基本组件。
在本教程中,您将创建用于返回数据的通用服务。服务是指可以重用以完成特定任务的任何代码。您的组件不需要知道服务如何获取其信息。它只需要知道该服务将返回一个Promise。在这种情况下,将使用 模拟数据请求setTimeout
,它将在提供数据之前等待指定的时间量。
在目录services
下创建一个名为的新src/
目录:
- mkdir src/services
该目录将保存您的异步函数。打开一个名为 的文件rivers.js
:
- nano src/services/rivers.js
在该文件中,导出一个getRiverInformation
返回承诺的函数。在 promise 中,添加一个setTimeout
函数,它将在1500
几毫秒后解析 promise 。这将给您一些时间来查看组件在等待数据解析时如何呈现:
export function getRiverInformation() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
continent: 'Africa',
length: '6,650 km',
outflow: 'Mediterranean'
})
}, 1500)
})
}
在此代码段中,您对河流信息进行了硬编码,但此函数将类似于您可能使用的任何异步函数,例如 API 调用。重要的部分是代码返回一个承诺。
保存并关闭文件。
现在您有了一个返回数据的服务,您需要将它添加到您的组件中。这有时会导致问题。假设您在组件内部调用了异步函数,然后使用useState
Hook将数据设置为变量。代码将是这样的:
import React, { useState } from 'react';
import { getRiverInformation } from '../../services/rivers';
export default function RiverInformation() {
const [riverInformation, setRiverInformation] = useState({});
getRiverInformation()
.then(d => {
setRiverInformation(d)
})
return(
...
)
}
当您设置数据时,Hook 更改将触发组件重新渲染。当组件重新渲染时,该getRiverInformation
函数将再次运行,当它解析时,它将设置状态,这将触发另一次重新渲染。循环将永远持续下去。
为了解决这个问题,React 有一个特殊的 Hook useEffect
,它只会在特定数据发生变化时运行。
的useEffect
钩子接受函数作为第一个参数和一个阵列触发器的作为第二个参数。该函数将在布局和绘制后的第一次渲染上运行。之后,它只会在触发器之一发生变化时运行。如果您提供一个空数组,它只会运行一次。如果不包含触发器数组,它将在每次渲染后运行。
打开RiverInformation.js
:
- nano src/components/RiverInformation/RiverInformation.js
使用useState
Hook 创建一个名为 的变量riverInformation
和一个名为 的函数setRiverInformation
。您将通过设置riverInformation
异步函数解析的时间来更新组件。然后用 包装getRiverInformation
函数useEffect
。确保传递一个空数组作为第二个参数。当承诺解决时,riverInformation
用setRiverInformation
函数更新:
import React, { useEffect, useState } from 'react';
import { getRiverInformation } from '../../services/rivers';
export default function RiverInformation() {
const [riverInformation, setRiverInformation] = useState({});
useEffect(() => {
getRiverInformation()
.then(data =>
setRiverInformation(data)
);
}, [])
return(
<div>
<h2>River Information</h2>
<ul>
<li>Continent: {riverInformation.continent}</li>
<li>Length: {riverInformation.length}</li>
<li>Outflow: {riverInformation.outflow}</li>
</ul>
</div>
)
}
异步函数解析后,使用新信息更新无序列表。
保存并关闭文件。当您执行此操作时,浏览器将刷新,您将在函数解析后找到数据:
请注意,组件在加载数据之前呈现。异步代码的优点是它不会阻塞初始渲染。在这种情况下,您有一个组件可以显示没有任何数据的列表,但您也可以渲染微调器或可缩放矢量图形 (SVG) 占位符。
有时您只需要加载一次数据,例如您正在获取用户信息或永远不会更改的资源列表。但是很多时候你的异步函数需要一些参数。在这些情况下,您需要useEffect
在数据发生变化时触发 use Hook。
要模拟这一点,请向您的服务添加更多数据。打开rivers.js
:
- nano src/services/rivers.js
然后添加一个包含更多河流数据的对象。根据name
参数选择数据:
const rivers = {
nile: {
continent: 'Africa',
length: '6,650 km',
outflow: 'Mediterranean'
},
amazon: {
continent: 'South America',
length: '6,575 km',
outflow: 'Atlantic Ocean'
},
yangtze: {
continent: 'Asia',
length: '6,300 km',
outflow: 'East China Sea'
},
mississippi: {
continent: 'North America',
length: '6,275 km',
outflow: 'Gulf of Mexico'
}
}
export function getRiverInformation(name) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(
rivers[name]
)
}, 1500)
})
}
保存并关闭文件。接下来,打开App.js
以便您可以添加更多选项:
- nano src/components/App/App.js
在 里面App.js
,创建一个有状态的变量和函数来用useState
Hook保存选定的河流。然后为每条河流添加一个带有onClick
处理程序的按钮以更新所选河流。使用名为的道具将传递river
给:RiverInformation
name
import React, { useState } from 'react';
import './App.css';
import RiverInformation from '../RiverInformation/RiverInformation';
function App() {
const [river, setRiver] = useState('nile');
return (
<div className="wrapper">
<h1>World's Longest Rivers</h1>
<button onClick={() => setRiver('nile')}>Nile</button>
<button onClick={() => setRiver('amazon')}>Amazon</button>
<button onClick={() => setRiver('yangtze')}>Yangtze</button>
<button onClick={() => setRiver('mississippi')}>Mississippi</button>
<RiverInformation name={river} />
</div>
);
}
export default App;
保存并关闭文件。接下来,打开RiverInformation.js
:
- nano src/components/RiverInformation/RiverInformation.js
拉入name
作为道具并将其传递给getRiverInformation
函数。一定要添加name
到数组中useEffect
,否则不会重新运行:
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { getRiverInformation } from '../../services/rivers';
export default function RiverInformation({ name }) {
const [riverInformation, setRiverInformation] = useState({});
useEffect(() => {
getRiverInformation(name)
.then(data =>
setRiverInformation(data)
);
}, [name])
return(
<div>
<h2>River Information</h2>
<ul>
<li>Continent: {riverInformation.continent}</li>
<li>Length: {riverInformation.length}</li>
<li>Outflow: {riverInformation.outflow}</li>
</ul>
</div>
)
}
RiverInformation.propTypes = {
name: PropTypes.string.isRequired
}
在此代码中,您还添加了一个带有 的弱类型系统PropTypes
,这将确保 prop 是一个字符串。
保存文件。当您这样做时,浏览器将刷新,您可以选择不同的河流。请注意单击和数据呈现之间的延迟:
如果您name
从useEffect
数组中遗漏了prop ,您将在浏览器控制台中收到构建错误。它会是这样的:
ErrorCompiled with warnings.
./src/components/RiverInformation/RiverInformation.js
Line 13:6: React Hook useEffect has a missing dependency: 'name'. Either include it or remove the dependency array react-hooks/exhaustive-deps
Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.
此错误告诉您效果中的函数具有您未明确设置的依赖项。在这种情况下,很明显效果不会起作用,但有时您可能会将 prop 数据与组件内的有状态数据进行比较,这可能会丢失对数组中项目的跟踪。
最后要做的是为您的组件添加一些防御性编程。这是一个强调应用程序高可用性的设计原则。您希望确保即使数据的形状不正确或者您根本没有从 API 请求中获取任何数据,您的组件也会呈现。
就像您的应用程序现在一样,效果将riverInformation
使用它收到的任何类型的数据更新。这通常是一个对象,但在它不是的情况下,您可以使用可选链来确保您不会抛出错误。
在里面RiverInformation.js
,用可选链接替换对象点链接的实例。要测试它是否有效,请{}
从useState
函数中删除默认对象:
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { getRiverInformation } from '../../services/rivers';
export default function RiverInformation({ name }) {
const [riverInformation, setRiverInformation] = useState();
useEffect(() => {
getRiverInformation(name)
.then(data =>
setRiverInformation(data)
);
}, [name])
return(
<div>
<h2>River Information</h2>
<ul>
<li>Continent: {riverInformation?.continent}</li>
<li>Length: {riverInformation?.length}</li>
<li>Outflow: {riverInformation?.outflow}</li>
</ul>
</div>
)
}
RiverInformation.propTypes = {
name: PropTypes.string.isRequired
}
保存并关闭文件。当你这样做时,即使代码引用属性undefined
而不是对象,文件仍然会加载:
防御性编程通常被认为是最佳实践,但当您无法保证响应时,它对异步函数(例如 API 调用)尤其重要。
在这一步中,您在 React 中调用了异步函数。您使用useEffect
Hook 在不触发重新渲染的情况下获取信息,并通过向useEffect
数组添加条件来触发新的更新。
在下一步中,您将对应用程序进行一些更改,以便它仅在安装时更新组件。这将帮助您的应用程序避免内存泄漏。
步骤 2 — 防止未安装组件上的错误
在此步骤中,您将阻止对已卸载组件的数据更新。由于您永远无法确定何时使用异步编程解析数据,因此在移除组件后数据将解析始终存在风险。更新已卸载组件上的数据效率低下,并且可能会导致内存泄漏,从而导致您的应用程序使用的内存超出所需。
到这一步结束时,您将知道如何通过在useEffect
Hook 中添加保护以仅在安装组件时更新数据来防止内存泄漏。
当前组件将始终被挂载,因此在从DOM 中删除组件后,代码不会尝试更新该组件,但大多数组件并不那么可靠。当用户与应用程序交互时,它们将被添加和从页面中删除。如果在异步函数解析之前从页面中删除了组件,则可能会发生内存泄漏。
要测试问题,请更新App.js
以添加和删除河流详细信息。
打开App.js
:
- nano src/components/App/App.js
添加一个按钮来切换河流的详细信息。使用useReducer
Hook 创建一个函数来切换细节和一个变量来存储切换状态:
import React, { useReducer, useState } from 'react';
import './App.css';
import RiverInformation from '../RiverInformation/RiverInformation';
function App() {
const [river, setRiver] = useState('nile');
const [show, toggle] = useReducer(state => !state, true);
return (
<div className="wrapper">
<h1>World's Longest Rivers</h1>
<div><button onClick={toggle}>Toggle Details</button></div>
<button onClick={() => setRiver('nile')}>Nile</button>
<button onClick={() => setRiver('amazon')}>Amazon</button>
<button onClick={() => setRiver('yangtze')}>Yangtze</button>
<button onClick={() => setRiver('mississippi')}>Mississippi</button>
{show && <RiverInformation name={river} />}
</div>
);
}
export default App;
保存文件。当您这样做时,浏览将重新加载,您将能够切换详细信息。
单击一条河流,然后立即单击“切换详细信息”按钮以隐藏详细信息。React 会生成一个错误警告,提示存在潜在的内存泄漏。
要解决此问题,您需要取消或忽略内部的异步函数useEffect
。如果您使用的是RxJS 之类的库,您可以在组件卸载时通过在useEffect
Hook 中返回一个函数来取消异步操作。在其他情况下,您需要一个变量来存储挂载状态。
打开RiverInformation.js
:
- nano src/components/RiverInformation/RiverInformation.js
在useEffect
函数内部,创建一个名为的变量mounted
并将其设置为true
. 在.then
回调中,如果mounted
为真,则使用条件来设置数据:
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { getRiverInformation } from '../../services/rivers';
export default function RiverInformation({ name }) {
const [riverInformation, setRiverInformation] = useState();
useEffect(() => {
let mounted = true;
getRiverInformation(name)
.then(data => {
if(mounted) {
setRiverInformation(data)
}
});
}, [name])
return(
<div>
<h2>River Information</h2>
<ul>
<li>Continent: {riverInformation?.continent}</li>
<li>Length: {riverInformation?.length}</li>
<li>Outflow: {riverInformation?.outflow}</li>
</ul>
</div>
)
}
RiverInformation.propTypes = {
name: PropTypes.string.isRequired
}
现在您有了变量,您需要能够在组件卸载时翻转它。使用useEffect
Hook,您可以返回一个将在组件卸载时运行的函数。返回一个设置mounted
为的函数false
:
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { getRiverInformation } from '../../services/rivers';
export default function RiverInformation({ name }) {
const [riverInformation, setRiverInformation] = useState();
useEffect(() => {
let mounted = true;
getRiverInformation(name)
.then(data => {
if(mounted) {
setRiverInformation(data)
}
});
return () => {
mounted = false;
}
}, [name])
return(
<div>
<h2>River Information</h2>
<ul>
<li>Continent: {riverInformation?.continent}</li>
<li>Length: {riverInformation?.length}</li>
<li>Outflow: {riverInformation?.outflow}</li>
</ul>
</div>
)
}
RiverInformation.propTypes = {
name: PropTypes.string.isRequired
}
保存文件。当您这样做时,您将能够在没有错误的情况下切换详细信息。
卸载时,组件会useEffect
更新变量。异步函数仍将解析,但不会对卸载的组件进行任何更改。这将防止内存泄漏。
在此步骤中,您仅在安装组件时才使应用程序更新状态。您更新了useEffect
Hook 以跟踪组件是否已安装并返回一个函数以在组件卸载时更新该值。
在下一步中,您将异步加载组件以将代码拆分为用户将根据需要加载的较小包。
第 3 步 – 使用Suspense
和延迟加载组件lazy
在这一步中,您将使用 ReactSuspense
和lazy
. 随着应用程序的增长,最终构建的大小也随之增长。您可以将代码拆分为更小的块,而不是强迫用户下载整个应用程序。反应Suspense
并lazy
与 webpack 和其他构建系统一起工作,将您的代码拆分成用户能够按需加载的更小的部分。将来,您将能够使用Suspense
加载各种数据,包括 API 请求。
在这一步结束时,您将能够异步加载组件,将大型应用程序分解为更小、更集中的块。
到目前为止,您只使用了异步加载数据,但您也可以异步加载组件。此过程通常称为代码拆分,有助于减小代码包的大小,因此如果您的用户只使用其中的一部分,则无需下载完整的应用程序。
大多数情况下,您静态导入代码,但您可以通过作为函数而不是语句调用来动态导入代码import
。代码将是这样的:
import('my-library')
.then(library => library.action())
React 为您提供了一组额外的工具,称为lazy
和Suspense
。ReactSuspense
最终会扩展以处理数据加载,但现在您可以使用它来加载组件。
打开App.js
:
- nano src/components/App/App.js
然后导入lazy
和Suspense
从react
:
import React, { lazy, Suspense, useReducer, useState } from 'react';
import './App.css';
import RiverInformation from '../RiverInformation/RiverInformation';
function App() {
const [river, setRiver] = useState('nile');
const [show, toggle] = useReducer(state => !state, true);
return (
<div className="wrapper">
<h1>World's Longest Rivers</h1>
<div><button onClick={toggle}>Toggle Details</button></div>
<button onClick={() => setRiver('nile')}>Nile</button>
<button onClick={() => setRiver('amazon')}>Amazon</button>
<button onClick={() => setRiver('yangtze')}>Yangtze</button>
<button onClick={() => setRiver('mississippi')}>Mississippi</button>
{show && <RiverInformation name={river} />}
</div>
);
}
export default App;
lazy
并且Suspsense
有两个不同的工作。您可以使用该lazy
函数动态导入组件并将其设置为变量。Suspense
是一个内置组件,用于在加载代码时显示回退消息。
替换import RiverInformation from '../RiverInformation/RiverInformation';
为对 的调用lazy
。将结果分配给名为 的变量RiverInformation
。然后{show && <RiverInformation name={river} />}
用Suspense
组件和一个<div>
带有消息Loading Component
的fallback
属性包裹:
import React, { lazy, Suspense, useReducer, useState } from 'react';
import './App.css';
const RiverInformation = lazy(() => import('../RiverInformation/RiverInformation'));
function App() {
const [river, setRiver] = useState('nile');
const [show, toggle] = useReducer(state => !state, true);
return (
<div className="wrapper">
<h1>World's Longest Rivers</h1>
<div><button onClick={toggle}>Toggle Details</button></div>
<button onClick={() => setRiver('nile')}>Nile</button>
<button onClick={() => setRiver('amazon')}>Amazon</button>
<button onClick={() => setRiver('yangtze')}>Yangtze</button>
<button onClick={() => setRiver('mississippi')}>Mississippi</button>
<Suspense fallback={<div>Loading Component</div>}>
{show && <RiverInformation name={river} />}
</Suspense>
</div>
);
}
export default App;
保存文件。当你这样做时,重新加载页面,你会发现组件是动态加载的。如果您想查看加载消息,可以在Chrome 网络浏览器中限制响应。
如果您导航到Chrome 或Firefox 中的网络选项卡,您会发现代码被分成不同的块。
每个 chunk 默认都有一个编号,但是 Create React App 结合 webpack,可以通过动态导入添加注释来设置 chunk 名称。
在 中App.js
,/* webpackChunkName: "RiverInformation" */
在import
函数内部添加注释:
import React, { lazy, Suspense, useReducer, useState } from 'react';
import './App.css';
const RiverInformation = lazy(() => import(/* webpackChunkName: "RiverInformation" */ '../RiverInformation/RiverInformation'));
function App() {
const [river, setRiver] = useState('nile');
const [show, toggle] = useReducer(state => !state, true);
return (
<div className="wrapper">
<h1>World's Longest Rivers</h1>
<div><button onClick={toggle}>Toggle Details</button></div>
<button onClick={() => setRiver('nile')}>Nile</button>
<button onClick={() => setRiver('amazon')}>Amazon</button>
<button onClick={() => setRiver('yangtze')}>Yangtze</button>
<button onClick={() => setRiver('mississippi')}>Mississippi</button>
<Suspense fallback={<div>Loading Component</div>}>
{show && <RiverInformation name={river} />}
</Suspense>
</div>
);
}
export default App;
保存并关闭文件。当您这样做时,浏览器将刷新并且RiverInformation
块将具有唯一名称。
在此步骤中,您异步加载组件。您使用lazy
和Suspense
动态导入组件并在组件加载时显示加载消息。您还为 webpack 块提供了自定义名称,以提高可读性和调试。
结论
异步函数创建高效的用户友好应用程序。然而,它们的优势伴随着一些微妙的代价,这些代价可能会演变成程序中的错误。您现在拥有的工具可以让您将大型应用程序拆分为较小的部分并加载异步数据,同时仍为用户提供可见的应用程序。您可以利用这些知识将 API 请求和异步数据操作合并到您的应用程序中,从而创建快速可靠的用户体验。
如果您想阅读更多 React 教程,请查看我们的React 主题页面,或返回“如何在 React.js 中编码”系列页面。