在React中获取数据的方法比较

136 阅读6分钟

生命周期方法、钩子、悬念:哪个最适合在React中获取?

在执行数据获取等I/O操作时,你必须启动获取操作,等待响应,将响应数据保存到组件的状态中,最后再渲染。

非同步数据获取需要额外的努力来适应React的声明性。React一步一步地改进,以尽量减少这种额外的努力。

生命周期方法、钩子和悬念是React中获取数据的方法。我将用例子和演示来描述它们,提炼出每一种方法的好处和坏处。

了解每种方法的来龙去脉,会让你更好地进行异步操作的编码。

1.使用生命周期方法获取数据

应用程序Employees.org要做2件事。

  1. 最初获取该公司的20名员工。
  2. 过滤那些名字中包含查询的员工。

Employees Application

在实现这些要求之前,回顾一下一个类组件的2个生命周期方法。

  1. componentDidMount():安装后执行一次
  2. componentDidUpdate(prevProps):当道具或状态改变时执行。

<EmployeesPage> 使用这2个生命周期方法来实现获取的逻辑。

jsx

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> 有一个异步方法 fetch()处理获取。当获取请求完成后,组件的状态会随着获取的 employeesthis.fetch()componentDidMount()生命周期方法中执行:当组件最初被渲染时,它开始获取雇员。

当用户在输入字段中输入查询时,query 道具被更新。每次发生这种情况时,this.fetch()componentDidUpdate() 执行:它实现了对雇员的过滤。

虽然生命周期方法相对容易掌握,但基于类的方法却存在模板代码和重用困难。

优点

直观
很容易理解:生命周期方法componentDidMount() 在第一次渲染时启动获取,componentDidUpdate() 在道具改变时重新获取数据。

缺点

模板代码
基于类的组件需要 "仪式 "代码:扩展React.Component ,在constructor() 内调用super(props) ,等等。

this
使用this 关键字的工作是很繁重的。

代码重复
componentDidMount()componentDidUpdate() 里面的代码大部分是重复的。

难以重用
雇员获取的逻辑在另一个组件中重用很复杂。

2.使用钩子获取数据

钩子是基于类的获取的一个更好的选择。作为简单的函数,钩子没有一个 "仪式 "代码,而且更容易重用。

让我们回顾一下useEffect(callback[, deps]) 钩子。这个钩子在安装后执行callback ,并在随后的渲染中当deps 发生变化时执行。

在下面的例子中,<EmployeesPage> 使用useEffect() 来获取雇员数据。

jsx

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} />;
}

你可以看到<EmployeesPage> ,与类的版本相比,使用钩子简化了不少。

<EmployeesPage> 功能组件中,useEffect(fetch, [query]) 在初始渲染后执行fetch 回调。另外,fetch 在以后的渲染中被调用,但只有在query 道具发生变化时才会被调用。

但仍有改进的余地。钩子允许你从<EmployeesPage> 组件中提取雇员的获取逻辑。让我们来做这件事:

jsx

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()

优点

简单明了
钩子是没有模板代码的,因为它们是简单的函数。

可重用性
在钩子中实现的获取逻辑很容易被重用。

缺点

入门门槛
钩子有一点反直觉,因此在使用前你必须对它们有所了解。钩子依赖于闭包,所以也要对其有充分的了解

势在必行
有了钩子,你仍然必须使用势在必行的方法来执行数据的获取。

3.使用悬念进行数据获取

Suspense提供了一种声明式的方法来在React中异步地获取数据。

注意:截至2019年11月,Suspense还处于实验阶段。

<Suspense> 封装了一个执行异步操作的组件:

jsx

<Suspense fallback={<span>Fetch in progress...</span>}>
  <FetchSomething />
</Suspense>

当获取正在进行时,Suspense渲染fallback 道具内容。稍后当获取完成后,suspense会将获取的数据渲染到<FetchSomething />

让我们看看员工的应用程序是如何使用suspense的:

jsx

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} />;
}

<EmployeesPage> 使用suspense来处理组件<EmployeesFetch> 内的获取工作。

resource.employees <EmployeesFetch> 里面是一个特别包装的承诺,在后台与 suspense 通信。这样suspense就知道要 "暂停 " 的渲染多长时间,当资源准备好了,就可以开始了。<EmployeesFetch>

这就是最大的胜利。Suspense以声明性和同步的方式处理异步操作

这些组件没有被如何获取数据的细节所困扰。相反,它们是以声明的方式使用资源来渲染内容。没有生命周期,没有钩子,没有async/await ,没有组件内部的回调:只是渲染一个资源。

优点

声明性
Suspense让你在React中声明性地执行异步操作。

简单性
声明式的代码很容易操作。组件不会被如何获取数据的细节所困扰。

与获取实现的松散耦合
使用悬念的组件不知道数据是如何获取的:使用REST或GraphQL。悬念设置了一个边界,保护获取的细节泄露到你的组件中。

没有竞赛条件
如果启动了多个获取操作,suspense使用最新的获取请求。

缺点

需要适配器
Suspense需要专门的获取库或适配器来实现suspense的获取接口。

4.主要启示

长期以来,生命周期方法一直是获取的唯一解决方案。然而,使用它们来获取有很多模板代码的问题,有重复,有重用的困难。

使用钩子获取是一个更好的选择:减少模板代码。

Suspense的好处是声明式获取。你的组件不会被获取的实现细节弄得一团糟。Suspense更接近于React本身的声明性本质。

你更喜欢哪种数据获取方式?