当页面数据来自于多个请求时,我们该怎么去处理呢?这也是一个很常见的场景,我们需要先考虑这几点:
- 请求之间无依赖关系,可以并发进行
- 请求之间存在依赖关系,需要依次进行
- 请求完成之前,页面显示 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 秒!
具体场景
有以下三个请求:
- 请求用户数据
- 请求 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 设计
基于 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>
);
}