国外文章, 笔者采用意译的方式, 以保证文章的可读性.
当执行像数据获取这样的 I/O 操作时, 你必须发起获取请求, 等待响应, 将响应数据保存到组件的状态中, 最后渲染. 异步的数据获取会要求额外的工作来适应 React 的声明性, React 也在逐步改进去最小化这种额外的工作. 生命周期函数, hooks 和 suspense 都是 React 中获取数据的方式, 我将在下面的示例中分别讨论它们的优缺点. 只有了解每种方式的具体细节才能帮助你更好地在代码中实现异步操作.
使用生命周期函数获取数据
下面这个应用必须要做两件事情:
(1) 初始化获取该公司的 20 位员工;
(2) 筛选出名称包含查询条件的员工.
image.PNG
在实现这些需求之前, 回顾一下类组件的 2 个生命周期方法:
(1) componentDidMount(): 在组件挂载后执行一次;
(2) componentDidUpdate(prevProps): 当 props 和 state 发生变化的时候执行.
- <EmployeesPage > 使用这两个生命周期函数来实现了数据获取逻辑:
- import EmployeesList from "./EmployeesList";
- import { fetchEmployees } from "./fake-fetch";
- class EmployeesPage extends Component {
- constructor(props) {
- super(props);
- this.state = { employees: [], isFetching: true };
- }
- componentDidMount() {
- this.fetch();
- }
- componentDidUpdate(prevProps) {
- if (prevProps.query !== this.props.query) {
- this.fetch();
- }
- }
- async fetch() {
- this.setState({ isFetching: true });
- const employees = await fetchEmployees(this.props.query);
- this.setState({ employees, isFetching: false });
- }
- render() {
- const { isFetching, employees } = this.state;
- if (isFetching) {
- return <div>Fetching employees....</div>;
- }
- return <EmployeesList employees={employees} />;
- }
- }
打开示例来探索一下 < EmployeesPage > 是如何来获取数据的.<EmployeesPage > 有一个异步的 fetch 方法用来获取数据, 当获取完成的时候, 使用获取的 employees 来更新组件的状态. this.fetch() 在 componentDidMount() 生命周期函数中执行: 当组件初始化渲染完成后开始获取员工数据.
当用户在这个输入框中输入查询条件后, query 属性会发生变更, 每次在它发生变化的时候, this.fetch() 都将会在 componentDidUpdate 生命周期函数中执行, 从而来实现员工筛选的功能. 虽然生命周期方法相对容易掌握, 但是基于类的方法存在样板代码和可重用性困难.
优点:
简便. 比较容易理解: 生命周期方法 componentDidMount 在组件第一次初始化渲染后发起获取数据的请求, 当 props 发生变化的时候通过 componentDidUpdate 来重新获取数据.
缺点:
(1) 样板代码. 基于类的组件的代码具有 "仪式感": 继承 React.Component, 并且需要在 constructor 中调用 super(props) 等等;
(2) this 问题. 使用 this 关键字很麻烦;
(3) 代码重复. 在 componentDidMount 和 componentDidUpdate 中的代码其实大部分都是重复的;
(4) 难以重用. 获取员工的逻辑如果是在其他的组件中就已经很难复用了.
2. 使用 hooks 获取数据
对于基于类的组件来说, hooks 是一个更好的选择. 作为一个普通函数来讲, hooks 摒弃了具有 "仪式感" 的代码并且更容易复用.
我们首先来回顾一下 useEffect(callback[, deps]) 钩子函数的使用. 这个钩子函数在组件挂载后会执行 callback, 当依赖项 deps 发生变化后会继续执行后续的渲染. 在下面的示例中 < EmployeesPage > 使用 useEffect() 来获取员工数据:
- import React, { useState } from 'react';
- import EmployeesList from "./EmployeesList";
- import { fetchEmployees } from "./fake-fetch";
- function EmployeesPage({ query }) {
- const [isFetching, setFetching] = useState(false);
- const [employees, setEmployees] = useState([]);
- useEffect(function fetch() {
- (async function() {
- setFetching(true);
- setEmployees(await fetchEmployees(query));
- setFetching(false);
- })();
- }, [query]);
- if (isFetching) {
- return <div>Fetching employees....</div>;
- }
- return <EmployeesList employees={employees} />;
- }
打开示例来看看 useEffect() 是如何来获取数据的. 你可以看到使用了 useEffect() 的版本要比类组件的版本简化得多. 在 EmployeesPage 函数式组件中 useEffect(fetch, [query]) 在组件第一次渲染后执行 fetch 回调, 并且只有在 query 属性发生变化后, 组件才会在重渲染后再次执行 fetch 回调. 但是我们还有提升的空间, hooks 允许你将获取员工的逻辑从 < EmployeesPage > 中提取出来, 我们来试试:
- import React, { useState } from 'react';
- import EmployeesList from "./EmployeesList";
- import { fetchEmployees } from "./fake-fetch";
- function useEmployeesFetch(query) {
- const [isFetching, setFetching] = useState(false);
- const [employees, setEmployees] = useState([]);
- useEffect(function fetch {
- (async function() {
- setFetching(true);
- setEmployees(await fetchEmployees(query));
- setFetching(false);
- })();
- }, [query]);
- return [isFetching, employees];
- }
- function EmployeesPage({ query }) {
- const [employees, isFetching] = useEmployeesFetch(query);
- if (isFetching) {
- return <div>Fetching employees....</div>;
- }
- return <EmployeesList employees={employees} />;
- }
我们的数据获取的逻辑已经被提取到 useEmployeesFetch() 中了. 组件 < EmployeesPage > 已经没有集成任何数据获取相关的逻辑, 而是专注于他最直接的工作: UI 渲染. 更重要的是, 你可以在任何需要获取员工数据的组件中复用 useEmployeesFetch().
优点
(1) 简单明了. hooks 没有样板代码的约束因为它们只是普通的函数;
(2) 复用性. hooks 中所实现的数据获取逻辑是很容易被复用的.
缺点
(1) 入门障碍. hooks 有点违反常规, 你在使用之前必须要理解他们, hooks 依赖闭包, 所以你也有必要将它们弄清楚.
(2) 命令式的. 使用 hooks, 你仍旧需要使用命令式的方式来执行数据获取.
使用 suspense 获取数据
suspense 提供了声明性的方式来在 React 中异步获取数据.
- <Suspense > 包装一个执行异步操作的组件:
- <Suspense fallback={<span>Fetch in progress...</span>}>
- <FetchSomething />
- </Suspense>
当正在获取数据的时候, suspense 会渲染 fallback 属性中的内容. 当数据获取完成后, suspense 会使用获取到的数据来渲染 < FetchSomething /> 组件. 我们来看看如何将 suspense 运用到员工应用中:
- import React, { Suspense } from "react";
- import EmployeesList from "./EmployeesList";
- function EmployeesPage({ resource }) {
- return (
- <Suspense fallback={<h1>Fetching employees....</h1>}>
- <EmployeesFetch resource={resource} />
- </Suspense>
- );
- }
- function EmployeesFetch({ resource }) {
- const employees = resource.employees.read();
- return <EmployeesList employees={employees} />;
- }
打开示例来检查 suspense 是如何工作的.<EmployeesPage > 使用 suspense 来处理组件 < EmployeesFetch > 中的数据获取逻辑.<EmployeesFetch > 中的 resource.employees 是一个特殊的 wrapped promise 用来在后台和 suspense 通信, 采用这种方式 suspense 可以知道在 < EmployeesFetch > 被渲染之前需要暂停多久, 并且知道当资源就绪后, 能继续渲染.
最大的亮点: Suspense 采用一种声明性和同步的方式来处理异步操作.
这些组件本身并没有集成任何数据获取相关的细节, 相反它们声明性地使用 resource 来渲染内容. 没有生命周期函数, 没有 hooks, 没有 async/await, 也没有在组件内部的回调: 仅仅只是渲染资源.
优点:
(1) 声明性的. suspense 允许你在 React 中以声明性的方式执行异步操作;
(2) 简单明了. 声明性代码使用起来更加简单, 组件本身并不关心数据获取的细节;
(3) 与获取逻辑的松散耦合. 因为使用 suspense 的组件本身是不知道如何获取数据的: 使用 REST 又或是 GraphQL.suspense 设定了一个边界用来保护数据获取的细节泄露到组件内部;
(4) 没有竞态条件. 如果同时开启了多个异步获取的操作, suspense 会使用最近一次的获取请求.
缺点:
需要适配器. suspense 需要已经实现了其 fetching 接口的专业的 fetching 库或者适配器.
4. 总结
生命周期函数曾经很长一段时间是数据获取的唯一手段. 然而通过这种方式会带来很多问题比如大量的样板代码, 重复和复用难度. 而使用 hooks 来获取数据是一个更好的选择: 我们减少了很多样板代码. suspense 的好处是声明性获取数据, 你的组件本身并不关心数据获取的细节, 同时 suspense 是最接近 React 本身的声明性理念的.
你更喜欢哪种获取数据的方式呢?
年初整理了一批 web 前端教程, 帮助想要成为 Web 前端程序员的人. 从零基础到各种框架的教程都有. 只需要加入到 Web 前端学习 qun:296,212,562. 即可免费领取, 学习过程中有任何问题可以在里面问. 种一颗树最好的时间是十年前, 其次是现在. 只要想学习, 不存在早晚.
来源: http://www.jianshu.com/p/230e7f1a0acb