如何使用 React 处理异步数据加载、懒加载和代码拆分

作者选择了 Creative Commons以作为 Write for Donations计划的一部分获得捐赠。

介绍

作为JavaScript的网络开发者,同步代码赋予了您运行部分代码的能力,而其他部分仍在等待数据或解析. 这意味着你的应用程序中的重要部分 将不必等待不重要的部分 然后再制作。 通过同步代码,您也可以通过请求并显示新信息来更新您的应用程序,即使长函数和请求正在背景处理中,也给用户一个平滑的经验.

在[反应] (https://reactjs.org/)开发中,同步编程呈现出独特的问题. 例如,当使用React [功能组件] (https://andsky.com/tech/tutorials/how-to-create-custom-components-in-react# step-4-%E2%80%94-building-a-functional-component)时,同步函数可以创建无限回路. 当一个组件加载时,它可以启动一个同步函数,当一个同步函数解决后,它可以触发一个会使组件召回一个同步函数的重置. 此教程将解释如何用一种特殊的[叫作`使用效果' (https://reactjs.org/docs/hooks-effect.html)来避免这种情况,该功能只有在特定数据变化时才能运行. 这会让您故意运行同步代码,而不是在每个渲染周期上运行.

同步代码并不限于对新数据的请求. React有一个内置系统,用于_lazy加载_组件,或者只在用户需要时才加载. 当结合Create React App中默认的 [webpack] (https://webpack.js.org/] 配置时,可以将代码分拆出来,将一个大应用程序缩小为可以按需要加载的更小的块. React有一个叫"Suspense"的特殊组件,在浏览器装入您的新组件时会显示占位符. 在React的未来版本中,您可以使用"Suspense"来将数据装入被嵌入的组件而不会被渲染出阻塞.

在本教程中,您将通过创建一个在河流上显示信息的应用程序来处理React中的非同步数据,并通过setTimeout模拟对Web APIs的请求。本教程结束时,您将能够使用useEffect头条加载非同步数据。

前提条件

步骤 1 — 使用useEffect加载非同步数据

在此步骤中,您将使用useEffect链接将非同步数据加载到样本应用程序中。您将使用链接防止不必要的数据采集,在数据加载时添加位置持有人,并在数据解决时更新组件。

要探索主题,您将创建一个应用程序,以显示有关世界上最长的河流的信息. 您将使用非同步函数加载数据,该函数模拟对外部数据源的请求。

首先,创建一个名为RiverInformation的组件,创建目录:

1mkdir src/components/RiverInformation

在文本编辑器中打开RiverInformation.js:

1nano src/components/RiverInformation/RiverInformation.js

然后添加一些位置持有人内容:

 1[label async-tutorial/src/components/RiverInformation/RiverInformation.js]
 2import React from 'react';
 3
 4export default function RiverInformation() {
 5  return(
 6    <div>
 7      <h2>River Information</h2>
 8    </div>
 9  )
10}

保存并关闭文件. 现在您需要导入并将新组件转换为您的 root 组件。

1nano src/components/App/App.js

通过在突出代码中添加导入并渲染组件:

 1[label async-tutorial/src/components/App/App.js]
 2import React from 'react';
 3import './App.css';
 4import RiverInformation from '../RiverInformation/RiverInformation';
 5
 6function App() {
 7  return (
 8    <div className="wrapper">
 9      <h1>World's Longest Rivers</h1>
10      <RiverInformation />
11    </div>
12  );
13}
14
15export default App;

保存并关闭文件。

最后,为了使应用程序更容易阅读,添加一些风格。

1nano src/components/App/App.css

通过将CSS替换为以下来添加一些包装到包装类:

1[label async-tutorial/src/components/App/App.css]
2.wrapper {
3    padding: 20px
4}

保存并关闭文件. 当您这样做时,浏览器将更新并渲染基本组件。

Basic Component, 1

在本教程中,您将创建通用 services 来返回数据. 服务是指任何可重复使用的代码来完成特定任务. 您的组件不需要知道该服务是如何获取信息的。

src/目录下创建一个名为服务的新目录:

1mkdir src/services

此目錄會保留您的非同步函數. 開啟名為 'rivers.js' 的檔案:

1nano src/services/rivers.js

在文件中,导出一个名为getRiverInformation的函数,返回承诺。在承诺中,添加一个setTimeout函数,在1500毫秒后解决承诺。

 1[label async-tutorial/src/services/rivers.js]
 2export function getRiverInformation() {
 3  return new Promise((resolve) => {
 4    setTimeout(() => {
 5      resolve({
 6        continent: 'Africa',
 7        length: '6,650 km',
 8        outflow: 'Mediterranean'
 9      })
10    }, 1500)
11  })
12}

在本片中,您正在硬编码河流信息,但此函数将类似于您可能使用的任何非同步函数,例如API调用。

保存并关闭文件。

现在您有一个返回数据的服务,您需要将其添加到您的组件中,这有时会导致问题,假设您在组件内呼叫了非同步函数,然后使用useState链接将数据设置为变量。

 1import React, { useState } from 'react';
 2import { getRiverInformation } from '../../services/rivers';
 3
 4export default function RiverInformation() {
 5  const [riverInformation, setRiverInformation] = useState({});
 6
 7  getRiverInformation()
 8  .then(d => {
 9    setRiverInformation(d)
10  })
11
12  return(
13    ...
14  )
15}

当您设置数据时,胡克变更会触发组件重现。当组件重现时,getRiverInformation函数将再次运行,当它解决时将设置状态,这将触发另一个重现。

为了解决这个问题,React 有一个名为useEffect的特殊,只有当特定数据发生更改时才会运行。

[ ] [ ] [ ] [ ] [ ] ] [ ] [ ] ] [ ] [ ] ] [ ] [ ] ] [ ] [ ] ] [ ] [ ] ] [ ] [ ] ] [ ] ] [ ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [ ] ] [

打开RiverInformation.js:

1nano src/components/RiverInformation/RiverInformation.js

使用useState来创建一个名为riverInformation的变量和一个名为setRiverInformation的函数。当非同步函数解决时,您将通过设置riverInformation来更新该组件。然后将getRiverInformation函数包裹到useEffect。请确保将空数列作为第二个参数。当承诺解决时,请用setRiverInformation函数更新riverInformation:

 1[label async-tutorial/src/components/RiverInformation/RiverInformation.js]
 2import React, { useEffect, useState } from 'react';
 3import { getRiverInformation } from '../../services/rivers';
 4
 5export default function RiverInformation() {
 6  const [riverInformation, setRiverInformation] = useState({});
 7
 8  useEffect(() => {
 9   getRiverInformation()
10   .then(data =>
11     setRiverInformation(data)
12   );
13  }, [])
14
15  return(
16    <div>
17      <h2>River Information</h2>
18      <ul>
19        <li>Continent: {riverInformation.continent}</li>
20        <li>Length: {riverInformation.length}</li>
21        <li>Outflow: {riverInformation.outflow}</li>
22      </ul>
23    </div>
24  )
25}

解决非同步函数后,更新一个未分类的列表新信息。

保存和关闭文件. 当你这样做时,浏览器将更新并在函数解决后找到数据:

River Information Updating After Load, 2

请注意,该组件在加载数据之前会渲染。非同步代码的优点是,它不会阻止初始渲染。在这种情况下,您有一个组件显示列表而没有任何数据,但您也可以渲染旋转器或可扩展的矢量图形(SVG)。

有时您只需要一次加载数据,例如如果您正在获取用户信息或从未改变的资源列表,但很多时候您的非同步函数将需要一些论点。

要模拟这一点,请为您的服务添加一些更多数据。

1nano src/services/rivers.js

然后添加一个包含几个河流的数据的 object

 1[label async-tutorial/src/services/rivers.js]
 2const rivers = {
 3 nile: {
 4   continent: 'Africa',
 5   length: '6,650 km',
 6   outflow: 'Mediterranean'
 7 },
 8 amazon: {
 9   continent: 'South America',
10   length: '6,575 km',
11   outflow: 'Atlantic Ocean'
12 },
13 yangtze: {
14   continent: 'Asia',
15   length: '6,300 km',
16   outflow: 'East China Sea'
17 },
18 mississippi: {
19   continent: 'North America',
20   length: '6,275 km',
21   outflow: 'Gulf of Mexico'
22 }
23}
24
25export function getRiverInformation(name) {
26  return new Promise((resolve) => {
27    setTimeout(() => {
28      resolve(
29        rivers[name]
30      )
31    }, 1500)
32  })
33}

保存并关闭文件。接下来,打开App.js,以便添加更多选项:

1nano src/components/App/App.js

App.js中,创建一个 stateful变量,并使用useState口袋来保持所选河流,然后为每个河流添加一个按钮,使用一个onClick处理器来更新所选河流。

 1[label async-tutorial/src/components/App/App.js]
 2import React, { useState } from 'react';
 3import './App.css';
 4import RiverInformation from '../RiverInformation/RiverInformation';
 5
 6function App() {
 7  const [river, setRiver] = useState('nile');
 8  return (
 9    <div className="wrapper">
10      <h1>World's Longest Rivers</h1>
11      <button onClick={() => setRiver('nile')}>Nile</button>
12      <button onClick={() => setRiver('amazon')}>Amazon</button>
13      <button onClick={() => setRiver('yangtze')}>Yangtze</button>
14      <button onClick={() => setRiver('mississippi')}>Mississippi</button>
15      <RiverInformation name={river} />
16    </div>
17  );
18}
19
20export default App;

保存并关闭文件。接下来,打开RiverInformation.js:

1nano src/components/RiverInformation/RiverInformation.js

名称拖入作为支撑,并将其传输到getRiverInformation函数中,请确保将名称添加到useEffect的数组中,否则它不会重新启动:

 1[label async-tutorial/src/components/RiverInformation/RiverInformation.js]
 2import React, { useEffect, useState } from 'react';
 3import PropTypes from 'prop-types';
 4import { getRiverInformation } from '../../services/rivers';
 5
 6export default function RiverInformation({ name }) {
 7  const [riverInformation, setRiverInformation] = useState({});
 8
 9  useEffect(() => {
10    getRiverInformation(name)
11    .then(data =>
12      setRiverInformation(data)
13    );
14  }, [name])
15
16  return(
17    <div>
18      <h2>River Information</h2>
19      <ul>
20        <li>Continent: {riverInformation.continent}</li>
21        <li>Length: {riverInformation.length}</li>
22        <li>Outflow: {riverInformation.outflow}</li>
23      </ul>
24    </div>
25  )
26}
27
28RiverInformation.propTypes = {
29 name: PropTypes.string.isRequired
30}

在这个代码中,你还添加了PropTypes的弱打字系统,这将确保支架是一个字符串。

保存文件. 当你这样做时,浏览器会更新,你可以选择不同的河流. 注意点击时和数据渲染时之间的延迟:

Update river information, 3

如果你在useEffect数组中错过了名称提示,你会在 浏览器控制台中收到构建错误。

1[secondary_label Error]
2Compiled with warnings.
3
4./src/components/RiverInformation/RiverInformation.js
5  Line 13:6: React Hook useEffect has a missing dependency: 'name'. Either include it or remove the dependency array react-hooks/exhaustive-deps
6
7Search for the keywords to learn more about each warning.
8To ignore, add // eslint-disable-next-line to the line before.

在这种情况下,很明显效果不会起作用,但有时您可能会将 prop 数据与组件内部的状态数据进行比较,从而可以丢失组件中的项目的跟踪。

最后要做的是将一些 defensive programming添加到您的组件中,这是一个设计原则,强调您的应用程序的高可用性。

因为你的应用现在,效果将更新riverInformation与它接收的任何类型的数据. 这通常将是一个对象,但在这种情况下,你可以使用可选的链接(https://andsky.com/tech/tutorials/js-v8-optional-chaining-nullish-coalescing)来确保你不会扔错误。

RiverInformation.js中,用可选的链接取代对象点链接的实例。

 1[label async-tutorial/src/components/RiverInformation/RiverInformation.js]
 2import React, { useEffect, useState } from 'react';
 3import PropTypes from 'prop-types';
 4import { getRiverInformation } from '../../services/rivers';
 5
 6export default function RiverInformation({ name }) {
 7  const [riverInformation, setRiverInformation] = useState();
 8
 9  useEffect(() => {
10    getRiverInformation(name)
11    .then(data =>
12      setRiverInformation(data)
13    );
14  }, [name])
15
16  return(
17    <div>
18      <h2>River Information</h2>
19      <ul>
20        <li>Continent: {riverInformation?.continent}</li>
21        <li>Length: {riverInformation?.length}</li>
22        <li>Outflow: {riverInformation?.outflow}</li>
23      </ul>
24    </div>
25  )
26}
27
28RiverInformation.propTypes = {
29  name: PropTypes.string.isRequired
30}

当你这样做时,文件仍然会加载,即使代码引用属性为未定义而不是对象:

River Information Updating After Load, 4

防御性编程通常被认为是最佳实践,但在非同步函数(如 API 调用)上尤其重要,当您无法保证响应时。

在此步骤中,您在 React 中调用了非同步函数. 您使用useEffect链接来获取信息,而无需触发重新渲染,并通过在useEffect阵列中添加条件来触发新的更新。

在下一步中,您将对您的应用程序进行一些更改,以便仅在安装时更新组件。

步骤2 - 防止未安装的组件出现错误

在此步骤中,您将防止未安装的组件的数据更新. 由于您永远无法确定何时使用非同步编程将数据解决,因此在删除组件后,数据将始终存在风险。

到本步骤结束时,您将知道如何通过在useEffect口袋中添加警卫来防止内存泄露,以便在组件安装时才更新数据。

目前的组件将始终安装,所以在从 DOM中删除后,代码不会尝试更新组件,但大多数组件不那么可靠。

要测试问题,请更新App.js,以便可以添加和删除河流细节。

打开App.js:

1nano src/components/App/App.js

添加一个按钮来切换河流的细节. 使用useReducer来创建一个函数来切换细节和一个变量来存储切换状态:

 1[label async-tutorial/src/components/App/App.js]
 2import React, { useReducer, useState } from 'react';
 3import './App.css';
 4import RiverInformation from '../RiverInformation/RiverInformation';
 5
 6function App() {
 7  const [river, setRiver] = useState('nile');
 8  const [show, toggle] = useReducer(state => !state, true);
 9  return (
10    <div className="wrapper">
11      <h1>World's Longest Rivers</h1>
12      <div><button onClick={toggle}>Toggle Details</button></div>
13      <button onClick={() => setRiver('nile')}>Nile</button>
14      <button onClick={() => setRiver('amazon')}>Amazon</button>
15      <button onClick={() => setRiver('yangtze')}>Yangtze</button>
16      <button onClick={() => setRiver('mississippi')}>Mississippi</button>
17      {show && <RiverInformation name={river} />}
18    </div>
19  );
20}
21
22export default App;

保存文件. 当你这样做时,浏览会重新加载,你将能够转移细节。

点击一个河流,然后立即点击 Toggle Details 按钮来隐藏细节。

Warning when component is updated after being removed, 5

要修复此问题,您需要取消或忽略useEffect中的非同步函数。如果您正在使用像 RxJS这样的库,则您可以通过返回useEffect口袋中的函数来取消非同步操作,当该组件卸载时,您可以使用变量来存储安装状态。

打开RiverInformation.js:

1nano src/components/RiverInformation/RiverInformation.js

useEffect函数中,创建一个名为mounted的变量并将其设置为true

 1[label async-tutorial/src/components/RiverInformation/RiverInformation.js]
 2
 3import React, { useEffect, useState } from 'react';
 4import PropTypes from 'prop-types';
 5import { getRiverInformation } from '../../services/rivers';
 6
 7export default function RiverInformation({ name }) {
 8  const [riverInformation, setRiverInformation] = useState();
 9
10  useEffect(() => {
11    let mounted = true;
12    getRiverInformation(name)
13    .then(data => {
14      if(mounted) {
15        setRiverInformation(data)
16      }
17    });
18  }, [name])
19
20  return(
21    <div>
22      <h2>River Information</h2>
23      <ul>
24        <li>Continent: {riverInformation?.continent}</li>
25        <li>Length: {riverInformation?.length}</li>
26        <li>Outflow: {riverInformation?.outflow}</li>
27      </ul>
28    </div>
29  )
30}
31
32RiverInformation.propTypes = {
33  name: PropTypes.string.isRequired
34}

现在你有变量,你需要能够在组件卸载时扭转它. 使用useEffect链接,你可以返回一个在组件卸载时运行的函数。

 1[label async-tutorial/src/components/RiverInformation/RiverInformation.js]
 2
 3import React, { useEffect, useState } from 'react';
 4import PropTypes from 'prop-types';
 5import { getRiverInformation } from '../../services/rivers';
 6
 7export default function RiverInformation({ name }) {
 8  const [riverInformation, setRiverInformation] = useState();
 9
10  useEffect(() => {
11    let mounted = true;
12    getRiverInformation(name)
13    .then(data => {
14      if(mounted) {
15        setRiverInformation(data)
16      }
17    });
18    return () => {
19     mounted = false;
20   }
21  }, [name])
22
23  return(
24    <div>
25      <h2>River Information</h2>
26      <ul>
27        <li>Continent: {riverInformation?.continent}</li>
28        <li>Length: {riverInformation?.length}</li>
29        <li>Outflow: {riverInformation?.outflow}</li>
30      </ul>
31    </div>
32  )
33}
34
35RiverInformation.propTypes = {
36  name: PropTypes.string.isRequired
37}

保存文件. 当您这样做时,您将能够在没有错误的情况下切换细节。

No warning when toggling, 6

当您卸载时,组件useEffect更新变量. 非同步函数仍会解决问题,但不会对未安装的组件进行任何更改。

在此步骤中,您只在安装组件时更新了应用程序更新状态,您更新了useEffect链接以追踪组件是否安装,并返回了一个函数以更新当组件卸载时的值。

在下一步中,您将无同步地加载组件,将代码分成更小的包,用户将根据需要加载。

步骤 3 — 懒惰地加载一个组件暂停懒惰

在此步骤中,您将使用 React Suspenselazy 来分割代码。随着应用程序的增长,最终构建的大小会随之增加。而不是强迫用户下载整个应用程序,您可以将代码分割成更小的块。 React Suspenselazy 与 webpack 和其他构建系统一起工作,将代码分割成更小的块,用户将能够按需求下载。

到此步骤结束时,您将能够非同步地加载组件,将大型应用程序分解为更小的、更集中的块。

到目前为止,您只使用了非同步加载数据,但您也可以使用非同步加载组件。这个过程通常被称为 代码分割,有助于减少代码包的大小,以便您的用户不必下载完整的应用程序,如果他们只使用部分。

大多数时候,你可以静态导入代码,但你可以导入代码 动态通过调用导入作为一个函数而不是一个陈述。

1import('my-library')
2.then(library => library.action())

React 提供了一组名为 lazy和 'Suspense' 的工具. React 'Suspense' 最终将扩展到 处理数据加载,但目前您可以使用它来加载组件。

打开App.js:

1nano src/components/App/App.js

然后从反应中导入懒惰暂停:

 1[label async-tutorial/src/components/App/App.js]
 2import React, { lazy, Suspense, useReducer, useState } from 'react';
 3import './App.css';
 4import RiverInformation from '../RiverInformation/RiverInformation';
 5
 6function App() {
 7  const [river, setRiver] = useState('nile');
 8  const [show, toggle] = useReducer(state => !state, true);
 9  return (
10    <div className="wrapper">
11      <h1>World's Longest Rivers</h1>
12      <div><button onClick={toggle}>Toggle Details</button></div>
13      <button onClick={() => setRiver('nile')}>Nile</button>
14      <button onClick={() => setRiver('amazon')}>Amazon</button>
15      <button onClick={() => setRiver('yangtze')}>Yangtze</button>
16      <button onClick={() => setRiver('mississippi')}>Mississippi</button>
17      {show && <RiverInformation name={river} />}
18    </div>
19  );
20}
21
22export default App;

懒惰暂停有两个不同的工作。你使用懒惰函数来动态导入组件并将其设置为变量。

Import RiverInformation../RiverInformation/RiverInformation代替为lazy。 将结果分配给名为RiverInformation的变量,然后将{show && <RiverInformation name={river} />}Suspense组件和Div>Loading Component的信息包裹到fallback口头:

 1[label async-tutorial/src/components/App/App.js]
 2import React, { lazy, Suspense, useReducer, useState } from 'react';
 3import './App.css';
 4const RiverInformation = lazy(() => import('../RiverInformation/RiverInformation'));
 5
 6function App() {
 7  const [river, setRiver] = useState('nile');
 8  const [show, toggle] = useReducer(state => !state, true);
 9  return (
10    <div className="wrapper">
11      <h1>World's Longest Rivers</h1>
12      <div><button onClick={toggle}>Toggle Details</button></div>
13      <button onClick={() => setRiver('nile')}>Nile</button>
14      <button onClick={() => setRiver('amazon')}>Amazon</button>
15      <button onClick={() => setRiver('yangtze')}>Yangtze</button>
16      <button onClick={() => setRiver('mississippi')}>Mississippi</button>
17      <Suspense fallback={<div>Loading Component</div>}>
18        {show && <RiverInformation name={river} />}
19      </Suspense>
20    </div>
21  );
22}
23
24export default App;

保存文件. 这样做后,重新加载页面,你会发现该组件已动态加载. 如果你想看到加载消息,你可以 throttleChrome 网页浏览器中的响应。

Component Loading

如果您浏览 Chrome 或 Firefox中的网络**选项卡,您会发现代码被分成不同的片段。

Chunks

每个片段都默认得到一个数字,但与Webpack相结合的Create React App,您可以通过动态导入添加评论来设置片段名称。

App.js中,在导入函数中添加webpackChunkName的评论:RiverInformation */`:

 1[label async-tutorial/src/components/App/App.js]
 2import React, { lazy, Suspense, useReducer, useState } from 'react';
 3import './App.css';
 4const RiverInformation = lazy(() => import(/* webpackChunkName: "RiverInformation" */ '../RiverInformation/RiverInformation'));
 5
 6function App() {
 7  const [river, setRiver] = useState('nile');
 8  const [show, toggle] = useReducer(state => !state, true);
 9  return (
10    <div className="wrapper">
11      <h1>World's Longest Rivers</h1>
12      <div><button onClick={toggle}>Toggle Details</button></div>
13      <button onClick={() => setRiver('nile')}>Nile</button>
14      <button onClick={() => setRiver('amazon')}>Amazon</button>
15      <button onClick={() => setRiver('yangtze')}>Yangtze</button>
16      <button onClick={() => setRiver('mississippi')}>Mississippi</button>
17      <Suspense fallback={<div>Loading Component</div>}>
18        {show && <RiverInformation name={river} />}
19      </Suspense>
20    </div>
21  );
22}
23
24export default App;

当你这样做时,浏览器将更新,而RiverInformation块将有一个独特的名称。

River Information Chunk

在此步骤中,您无同步地加载了组件. 您使用懒惰暂停来动态导入组件,并在组件加载时显示加载消息。

结论

非同步函数创建高效的用户友好的应用程序. 然而,它们的优点伴随着一些微妙的成本,可以演变为程序中的错误. 现在您有工具,可以让您将大型应用程序分割成更小的部件,并加载非同步数据,同时为用户提供可见的应用程序。

如果您想阅读更多 React 教程,请查看我们的 React 主题页面,或返回 如何在 React.js 系列中编码页面

Published At
Categories with 技术
comments powered by Disqus