React 最佳实践:处理多个数据源

1,918 阅读5分钟

当页面数据来自于多个请求时,我们该怎么去处理呢?这也是一个很常见的场景,我们需要先考虑这几点:

  • 请求之间无依赖关系,可以并发进行
  • 请求之间存在依赖关系,需要依次进行
  • 请求完成之前,页面显示 Loading 状态

说到请求,不如先来简单讲讲 Promise 和 async/await 叭!

Promise

Promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值。

一个 Promise 必然处于以下几种状态之一:

  • 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
  • 已兑现(fulfilled): 意味着操作成功完成。
  • 已拒绝(rejected): 意味着操作失败。

链式调用:resolve 函数对应 then;reject 函数对应 catch;

async/await

async 和 await 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 promise。

async 关键字

async function hello() {
  return 'Hello';
}
// let hello = async function() { return "Hello" };
// let hello = async () => {
//   return 'Hello';
// };
hello();

调用该函数会返回一个 promise。这是异步函数的特征之一 —— 它保证函数的返回值为 promise。

在函数声明为 async 时,JavaScript 引擎会添加必要的处理,以优化你的程序。

await 关键字

当 await 关键字与异步函数一起使用时,它的真正优势就变得明显了 —— 事实上, await 只在异步函数里面才起作用。 它可以放在任何异步的,基于 promise 的函数之前。它会暂停代码在该行上,直到 promise 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了。

缺陷

Async/await 让你的代码看起来是同步的,在某种程度上,也使得它的行为更加地同步。 await 关键字会阻塞其后的代码,直到 promise 完成,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但您自己的代码被阻塞。

这意味着您的代码可能会因为大量 await 的 promises 相继发生而变慢。每个 await 都会等待前一个完成,而你实际想要的是所有的这些 promises 同时开始处理(就像我们没有使用 async/await 时那样)。

有一种模式可以缓解这个问题——通过将 Promise 对象存储在变量中来同时开始它们,然后等待它们全部执行完毕。让我们看一些证明这个概念的例子。

async function timeTest() {
  await timeoutPromise(3000);
  await timeoutPromise(3000);
  await timeoutPromise(3000);
}

总运行时间大约为 9 秒。

async function timeTest() {
  const timeoutPromise1 = timeoutPromise(3000);
  const timeoutPromise2 = timeoutPromise(3000);
  const timeoutPromise3 = timeoutPromise(3000);

  await timeoutPromise1;
  await timeoutPromise2;
  await timeoutPromise3;
}

在这里,我们将三个 Promise 对象存储在变量中,这样可以同时启动它们关联的进程。总运行时间仅超过 3 秒!

具体场景

request-1.png

有以下三个请求:

  • 请求用户数据
  • 请求 province 列表
  • 请求 cities 列表

他们之间的逻辑关系是:

  • 首先初始化用户信息,包括姓名、省份、城市
  • 切换省份时,更新对应的城市列表

实现过程

模拟请求

首先,我们用 setTimeout 模拟上述三个请求:

获取用户信息(fetchUserInfo)

const fetchUserInfo = async () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({
        userName: 'Nate',
        province: 'shanghai',
        city: 'pudong',
      });
    }, 2000);
  });
};

获取省份列表(fetchProvices)

const fetchProvices = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { name: '北京', key: 'beijing' },
        { name: '上海', key: 'shanghai' },
        { name: '江苏', key: 'jiangsu' },
        { name: '山东', key: 'shandong' },
      ]);
    }, 2000);
  });
};

获取城市列表(fetchCities)

const fetchCities = (province) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(
        {
          beijing: [
            { name: '朝阳', key: 'chaoyang' },
            { name: '海淀', key: 'haidian' },
          ],
          shanghai: [
            { name: '浦东', key: 'pudong' },
            { name: '徐汇', key: 'xuhui' },
          ],
          jiangsu: [
            { name: '南京', key: 'nanjing' },
            { name: '苏州', key: 'suzhou' },
          ],
          shandong: [
            { name: '青岛', key: 'qingdao' },
            { name: '德州', key: 'dezhou' },
          ],
        }[province],
      );
    }, 2000);
  });
};

UI 设计

request-2.png

基于 Ant Design,主要用到了 Form, Select, Input 组件。 列表项默认为 loading 状态。

import { Form, Select, Input } from 'antd';

const { Option } = Select;

export default function MultipleRequestAsync(props) {
  return (
    <Form
      labelCol={{ span: 8 }}
      wrapperCol={{ span: 8 }}
      initialValues={{ province: 'loading...', city: 'loading...' }}
    >
      <Form.Item label="姓名:" name="userName">
        <Input placeholder="user name" />
      </Form.Item>
      <Form.Item label="Province:" name="province">
        <Select style={{ width: 120 }}>
          <Option value="loading">loading...</Option>
        </Select>
      </Form.Item>
      <Form.Item label="City:" name="city">
        <Select style={{ width: 120 }}>
          <Option value="loading">loading...</Option>
        </Select>
      </Form.Item>
    </Form>
  );
}

数据处理

初始化数据时,三个请求之间存在依赖关系:

  • 获取数据

    const [userInfo, setUserInfo] = useState({});
    const [provinces, setProvinces] = useState([]);
    const [cities, setCities] = useState([]);
    
    useEffect(() => {
      async function getData() {
        const info = await fetchUserInfo();
        setUserInfo(info);
        const provinces = await fetchProvices();
        setProvinces(provinces);
        const cities = await fetchCities(info.province);
        setCities(cities);
        form.setFieldsValue(info);
      }
      getData();
    }, []);
    
  • 渲染数据

    • 省份列表

      <Select style={{ width: 120 }} onChange={handleProvinceChange}>
        {provinces.length ? (
          provinces.map((provinces) => (
            <Option key={provinces.key} value={provinces.key}>
              {provinces.name}
            </Option>
          ))
        ) : (
          <Option value="loading">loading...</Option>
        )}
      </Select>
      
    • 城市列表

      <Select style={{ width: 120 }}>
        {cities.length ? (
          cities.map((city) => (
            <Option key={city.key} value={city.key}>
              {city.name}
            </Option>
          ))
        ) : (
          <Option value="loading">loading...</Option>
        )}
      </Select>
      
  • 切换省份时,更新对应的城市列表

    const [form] = Form.useForm();
    
    const handleProvinceChange = async (newProvince) => {
      // 显示中间loading状态
      form.setFieldsValue({ city: 'loading' });
      // 拉取城市列表
      const cities = await fetchCities(newProvince);
      setCities(cities);
      // 设置默认选择值
      form.setFieldsValue({ city: cities[0].key });
    };
    

完整代码

import { useState, useEffect } from 'react';
import { Form, Select, Input, Button } from 'antd';

const { Option } = Select;

const fetchUserInfo = async () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({
        userName: 'Nate',
        province: 'shanghai',
        city: 'pudong',
      });
    }, 2000);
  });
};

const fetchProvices = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { name: '北京', key: 'beijing' },
        { name: '上海', key: 'shanghai' },
        { name: '江苏', key: 'jiangsu' },
        { name: '山东', key: 'shandong' },
      ]);
    }, 2000);
  });
};

const fetchCities = (province) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(
        {
          beijing: [
            { name: '朝阳', key: 'chaoyang' },
            { name: '海淀', key: 'haidian' },
          ],
          shanghai: [
            { name: '浦东', key: 'pudong' },
            { name: '徐汇', key: 'xuhui' },
          ],
          jiangsu: [
            { name: '南京', key: 'nanjing' },
            { name: '苏州', key: 'suzhou' },
          ],
          shandong: [
            { name: '青岛', key: 'qingdao' },
            { name: '德州', key: 'dezhou' },
          ],
        }[province],
      );
    }, 2000);
  });
};

export default function MultipleRequestAsync(props) {
  const [userInfo, setUserInfo] = useState({});
  const [provinces, setProvinces] = useState([]);
  const [cities, setCities] = useState([]);
  const [form] = Form.useForm();

  useEffect(() => {
    async function getData() {
      const info = await fetchUserInfo();
      setUserInfo(info);
      const provinces = await fetchProvices();
      setProvinces(provinces);
      const cities = await fetchCities(info.province);
      setCities(cities);
      form.setFieldsValue(info);
    }
    getData();
  }, []);

  const handleProvinceChange = async (newProvince) => {
    form.setFieldsValue({ city: 'loading' });
    const cities = await fetchCities(newProvince);
    setCities(cities);
    form.setFieldsValue({ city: cities[0].key });
  };

  return (
    <Form
      form={form}
      labelCol={{ span: 8 }}
      wrapperCol={{ span: 8 }}
      initialValues={{ province: 'loading...', city: 'loading...' }}
    >
      <Form.Item label="姓名:" name="userName">
        <Input placeholder="user name" />
      </Form.Item>
      <Form.Item label="Province:" name="province">
        <Select style={{ width: 120 }} onChange={handleProvinceChange}>
          {provinces.length ? (
            provinces.map((provinces) => (
              <Option key={provinces.key} value={provinces.key}>
                {provinces.name}
              </Option>
            ))
          ) : (
            <Option value="loading">loading...</Option>
          )}
        </Select>
      </Form.Item>
      <Form.Item label="City:" name="city">
        <Select style={{ width: 120 }}>
          {cities.length ? (
            cities.map((city) => (
              <Option key={city.key} value={city.key}>
                {city.name}
              </Option>
            ))
          ) : (
            <Option value="loading">loading...</Option>
          )}
        </Select>
      </Form.Item>
    </Form>
  );
}

React 最佳实践