# async 和 await 异步编程解决方案
TIP
从本节内容我们开始学习未来实际开发中每天都会用到的 async 和 await 的相关知识 及 在实际开发中的注意事项、项目中的综合应用实践等。
async 和 await 的基本用法
- async/await 是什么 ?
- async 关键字的基本用法
- await 关键字的基本用法
- async 函数内部的执行流程
- async 函数与 await 的简单应用
深入学习 async 函数
- async 函数的返回值
- async 函数返回的 Promise 状态的改变
- async 函数内部的错误处理
- async 函数的各种写法
深入学习 await 关键字
- await 的值
- await 的注意事项
async 与 await 处理继发与并发
- async/await 处理继发问题
- async/await 处理并发问题
- 判断继发与并发
- 判断以下代码输出的结果
async 与 await 在实际项目中的实战应用
- 页面加载进度条
- 动态加载二级菜单
为什么需要 async 和 await
能过分析回调函数、Promise、Generator、async/await 实现异步的优缺点来总结为什么需要 async 和 await
# 一、async/await 基本用法
TIP
深入浅出 async/await 是什么,关键词的基本用法,async 的内部执行流程,async 与 await 的简单应用。
# 1、async/await 是什么?
TIP
async、await 是 ES2017 新增加的两个关键字,使得异步操作变得更加方便。
# 2、async 关键字基本用法
TIP
使用 async 关键字,可以声明一个 async 函数,表示函数里有异步操作
// 声明一个async函数
async function foo() {
console.log(2);
}
foo();
# 3、await 关键字基本用法
TIP
- await 是 async wait 的简写,表示 异步等待
- 正常情况下,await 后面是一个 Promise 对象,表示等待一个异步操作,当然也可以是其它值
- 一个 async 函数中可以多个 await
async function foo() {
await Promise.resolve(1);
await 2;
}
- await 不能出现在普通函数内,一般与 async 函数一起配合使用,但是 async 函数中可以没有 await
// 错误写法
function foo() {
await 2;
}
// 错误写法
async function foo() {
const arr=[1,2,3];
// await不能出现在非async的函数中
arr.forEach((item)=>{
await item
})
}
温馨提示
await 是离不开 async 的,但 async 中可以没有 await,同时 async 和 await 经常需要与 Promise 对象结合使用
# 4、async 函数内部的执行流程
TIP
- 当调用 async 函数时,代码从上往下执行,遇到 await 关键字后,就需要等待 await 后面的异步操作完成,才接着往下执行函数体内后面的语句。
- async 函数内部代码是同步的,await 会阻塞后续代码的执行,但 async 函数本身是异步的,所以执行 async 函数并不会阻塞后续代码的执行
console.log(1);
async function foo() {
console.log(2);
await Promise.resolve();
console.log(3);
await Promise.resolve();
console.log(4);
}
foo();
console.log(5);
// 最后输出结果: 1 2 5 3 4
- 如果遇到 return 或抛出错误,则后面的代码都不执行了
console.log(1);
async function foo() {
console.log(2);
await Promise.resolve();
console.log(3);
// return后面的代码都不执行了
return;
await Promise.resolve();
console.log(4);
}
foo();
console.log(5);
// 最后输出结果: 1 2 5 3
- 如果 await 后面的是一个失败的 Promise,则失败后的代码都不会执行。
console.log(1);
async function foo() {
console.log(2);
await Promise.reject();
console.log(3);
await Promise.resolve();
console.log(4);
}
foo();
console.log(5);
# 5、async 与 await 的简单应用
TIP
利用 async 和 await 实现多个异步操作
- 先用 Promise 和 setTimeout 来实现一个休眠函数,用来模拟异步任务
- 休眠函数你可以理解为等待一定时间后,再执行相关的代码
<script>
// 休眠函数
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
// async函数实现多个异步操作
async function foo() {
await sleep(1000);
console.log("做第一件事");
await sleep(2000);
console.log("做第二件事");
await sleep(1000);
console.log("做第三件事");
}
// 调用函数
foo();
</script>
# 二、深入学习 async 函数
TIP
深入浅出 async 函数的返回值,函数的各种形式,async 函数在实际开发中的注意事项
# 1、async 函数的返回值
TIP
async 函数一定返回一个 Promise 对象。如果一个 async 函数的返回值不是一个 Promise 对象,那么它将会被隐式地包装在一个成功的 Promise 中。
- 没有用 return 返回值 相当于返回
Promise.resolve(udnefined)
async function foo() {}
foo().then((data) => {
console.log(data); // undefined
});
- return 后面的值非 Promise 对象,则会包装在一个成功的 Promise 对象中返回
async function foo() {
return 2;
}
foo().then((data) => {
console.log(data); // 2
});
- return 后面的值为 Promise 对象,则直接将这个 Promise 对象返回
async function foo() {
return Promise.resolve("成功");
}
foo().then((data) => {
console.log(data); // 成功
});
- async 函数内抛出错误,则错误的内容会被包装在一个失败的 Promise 对象中返回
async 函数中 await 后面的 Promise 如果是一个失败的 Promise,则 async 函数的返回值就是这个失败的 Promise。在这之后的代码都不会执行
async function foo() {
throw new Error("错误");
// await Promise.reject("错误的Promise")
console.log(2);
}
foo().catch((e) => {
console.log(e);
});
# 2、async 函数返回的 Promise 状态的改变
TIP
async 函数返回的 Promise 对象必须等到内部所有 await 命令后面的 Promise 对象执行完才会发生状态改变,除非遇到 return 语名或者抛出错误。
简单理解就是
只有 async 函数内部的异步操作全部执行完,才会执行 async 函数返回的 Promise 对象 then 方法指定的回调函数。
// 休眠函数
function sleep(ms, value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
async function foo() {
await sleep(1000);
console.log("做第一件事");
// 遇到reutrn 或抛出错误之后的代码都不会执行
// return;
// throw new Error("抛出错误");
await sleep(2000);
console.log("做第二件事");
await sleep(1000);
console.log("做第三件事");
}
// 调用函数
foo().then((data) => {
console.log("最后执行我");
});
# 3、错误处理
TIP
当 async 函数内抛出错误时,可以通过try ... catch
或 Promise 的 catch 方法来处理错误。
# 3.1、catch 处理错误
TIP
当 await 后面的 Promise 是一个失败的 Promise 时,可以在 async 函数返回的 Promise 的 catch 方法中被处理。
这种方式处理错误,抛出错误后整个 async 函数都会中断执行。
async function f() {
await Promise.reject("出错了");
console.log("不执行了");
}
f()
.then((data) => console.log(data))
.catch((err) => console.log(err)); // 出错了
在当前失败的 Promise 的 catch 方法中处理,并不会影响后续代码的执行
async function f() {
await Promise.reject("出错了").catch((e) => {
console.log(e);
});
console.log("正常执行了");
}
f();
// 最终执行结果: 出错了 正常执行了
# 3.2、try...catch 处理错误
- 以下形式捕获错误,并不会影响后续代码的执行
async function f() {
try {
await Promise.reject("出错了");
} catch (e) {
console.log(e);
}
console.log("正常执行了");
}
f();
- 如果有多个 await 可以统一用一个
try...catch
来处理
以下写法,相当于多个异步任务是继发关系,后面的 Promise 需要等前面的执行完才能执行,只要有一个出错了,那后面的就没有执行的必要。所以可以用一个
try....catch
来统一处理。
async function f() {
try {
await Promise.resolve(1);
await Promise.reject("出错了");
await Promise.resolve(2);
} catch (e) {
console.log(e);
}
console.log("正常执行了");
}
f();
# 3.3、try...catch,实现多次重复尝试
<script type="module">
import ajax from "./ajax.js";
const url =
"https://www.fastmock.site/mock/6ec78e345df340241e1f5043f0167833/icode/test";
(async () => {
for (let i = 0; i < 3; i++) {
try {
const res = await ajax("get", url);
console.log(res);
break;
} catch (e) {}
console.log(`第${i + 1}次连接失败`);
}
})();
</script>
# 4、asycn 函数的各种写法
本节内容作为了解,知道基础的语法会使用即可
- async 函数 - 函数声明式写法
// async 函数
async function foo() {}
- async 函数 - 函数表达式写法
// async函数 的 函数表达式写法
const foo = async function () {};
- async 函数 - 箭头函数写法
// async 函数 的 箭头函数写法
const foo = async () => {};
// async 函数 的 箭头函数写法
const foo = async (param) => {};
- async 函数 - 对象的方法
const obj = {
// 普通写法
foo: function () {},
// async 函数写法
foo: async function () {},
// 普通写法
foo: () => {},
// async 函数写法
foo: async () => {},
// 普通写法
foo() {},
// async 函数写法
async foo() {},
};
- async 函数 - Class 的方法
class Person {
// 普通方法
// foo() {}
// async 函数写法
async foo() {}
}
// 方法调用 和 一般的方法调用一样,没有任何区别
new Person().foo();
# 三、深入学习 await 关键字
TIP
深入浅出 await 的机制,await 的值,await 在实际开发中的注意事项等
前面我们学习了 await 关键字的基本的用法,他需要配合 async 函数一起来使用。
await 一般只能出现在 async 函数中,但 async 函数中可以没有 await
# 1、await 的值
TIP
- await 关键字后面通常是一个 Promise 对象,await 的值就是该 Promise 对象的结果(PromiseResult)如果是一个失败的 Promise,则抛出错误
- 如果 await 后面不是 Promise 对象,await 的值就是该值,相当于包了一层
Promise.resolve()
之后在获取该 Promise 对象的结果
async function foo() {
const x = await Promise.resolve(1);
const y = await 2;
console.log(x, y);
}
foo();
async function foo() {
let x;
try {
x = await Promise.reject(1);
} catch (e) {
// ...
}
const y = await 2;
console.log(x, y); // undefined 2
}
foo();
# 2、await 注意事项
注:
- async 函数内部所有 await 后面的 Promise 对象都成功,async 函数返回的 Promise 对象才会成功;只要任何一个 await 后面的 Promise 对象失败,那么 async 函数返回的 Promise 对象就会失败
- 可以通过
try ... catch
或Promise ... catch
的方式来处理错误 - await 一般只能用在 async 函数中,async 函数中可以没有 await
- 有的浏览器也可以用在模块的最顶层,借用 await 解决模块异步加载的问题
# 四、async 与 await 处理继发与并发
TIP
深入浅出使用 async、await 处理继发问题 和 并发问题,继发和并发在我们实际开发中是非常常见的。
- 继发: 异步操作是有先后顺序的,只有完成了前一个才能执行后一个,它们之间是有先后关系的
- 并发: 同样发送请求,多次请求之间并没有先后的关系,同时发送请求,这就是并发
# 1、async/await 处理继发问题
TIP
多个异步请求有先后关系,后一个请求需要前一个请求的结果,如果前一个请求失败了,后面的请求也就不用发送了。
具体实现思路如下:
async function foo() {
try {
// 发送两个请求,前一个请求后的结果作为下一个请求的参数
let id = await Promise异步操作();
let result = await Promise异步操作(id);
// 最后输出结果
console.log(result);
} catch (e) {
// 错误处理
}
}
案例演示
- 先发一个请求,2s 后拿到参数 num
- 根据前一个请求的 num 值,再发一个请求,来获取对应的信息
<script type="module">
// 导入ajax模块
import { ajax, sleep } from "./ajax.js";
// async 异步函数
async function getInfo() {
let url =
"https://www.fastmock.site/mock/6ec78e345df340241e1f5043f0167833/icode/users/list";
// 利用try...catch捕获错误
try {
const id = await sleep(2000);
const info = await ajax("get", `${url}?num=${id}`);
// 打印最终结果
console.log(info.data);
} catch (e) {
console.log(e);
}
}
// 调用async函数
getInfo();
</script>
/**
* @param method 表示请求的方法,如get或post
* @param url 请求的地址
* @param body 如果为post请求,传入的请求体数据,需要传入JSON格式
*/
function ajax(method, url, body = null) {
// 返回Promise对象
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener("load", () => {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
resolve(xhr.response);
} else {
reject("请求失败");
}
});
// 响应过来的数据类型为json格式接受
xhr.responseType = "json";
xhr.open(method, url);
xhr.setRequestHeader("Content-Type", "application/json"); // 发送JSON格式数据
xhr.send(body);
});
}
// 休眠函数
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, ms);
});
}
export { ajax, sleep };
# 2、async/await 处理并发问题
TIP
多次请求之间并没有先后的关系,同时发送多个请求,其中一个请求出错了,不影响其它请求
具体实现思路如下:
// 第一种思路
async function foo() {
// 多个请求,并行请求
const p1 = Promise异步操作().catch((e) => console.log(e));
const p2 = Promise异步操作().catch((e) => console.log(e));
const res1 = await p1;
const res2 = await p2;
// 如果res1存在,则做相关操作
if (res1) {
}
// 如果res2存在,做相关操作
if (res2) {
}
}
// 第二种思路
async function foo() {
const [res1, res2] = await Promise.all([
Promise异步操作1().catch((e) => {}),
Promise异步操作2().catch((e) => {}),
]);
// 如果请求成功,有值,则做相关操作
if (res1) {
}
if (res2) {
}
}
案例演示
同时发送三个请求,来获取三条用户信息
<!--第一种思路-->
<script type="module">
import { ajax } from "./ajax.js";
async function getInfo() {
const url =
"https://www.fastmock.site/mock/6ec78e345df340241e1f5043f0167833/icode/users/list";
const p1 = ajax("get", `${url}?num=1`).catch((e) => console.log(e));
const p2 = ajax("get", `${url}?num=2`).catch((e) => console.log(e));
const p3 = ajax("get", `${url}?num=3`).catch((e) => console.log(e));
const res1 = await p1;
const res2 = await p2;
const res3 = await p3;
// 如果请求成功,有值,则做相关操作
if (res1) {
console.log(res1);
}
if (res2) {
console.log(res2);
}
if (res3) {
console.log(res3);
}
}
// 调用async函数
getInfo();
</script>
<script type="module">
import { ajax } from "./ajax.js";
async function getInfo() {
const url =
"https://www.fastmock.site/mock/6ec78e345df340241e1f5043f0167833/icode/users/list";
let arr = [1, 2, 3];
arr = arr.map((id) =>
ajax("get", `${url}?num=${id}`).catch((e) => console.log(e))
);
// arr.unshift(Promise.reject(2).catch((e) => {}));
const [res1, res2, res3] = await Promise.all(arr);
// 如果请求成功,有值,则做相关操作
if (res1) {
console.log(res1);
}
if (res2) {
console.log(res2);
}
if (res3) {
console.log(res3);
}
}
// 调用async函数
getInfo();
</script>
注:
以上代码实现了多个请求并发执行,但是存在一点不完美的地方,就是需要等多个并发都执行完后,才能统一对结果做处理。
但如果想要请求并发执行,并且那个先执行完,就可以接着做相关的后续事情,上面两种写法是做不到的,我们接着往下看,还有什么好的解决办法。
# 3、判断继发与并发
TIP
判断以下代码中多个请求进并发还是继发
// 以下代码是伪代码,并不能执行
async function foo() {
const arr = [1, 2, 3];
for (let id of arr) {
await ajax("get", url);
}
}
async function bar() {
const arr = [1, 2, 3];
arr.forEach(async (id) => {
await ajax("get", url);
});
}
# 3.1、代码解析
TIP
foo 函数中,多个请求是继发执行的,因为所有的 await 都是直接写在 foo 函数中,前一个 await 会阻塞后续代码的执行。
相当于如下写法
async function foo() {
await ajax("get", url);
await ajax("get", url);
await ajax("get", url);
}
bar 函数中,多个请求是并发执行,因为所有的 await 分别处于不同的 async 函数中。相当于在 bar 函数中调用了多个 async 函数,而 async 函数本身是异步的,并不会阻塞后续代码的执行。
相当于如下写法:
async function bar() {
(async (id) => {
const res1 = await ajax("get", url);
})();
(async (id) => {
const res1 = await ajax("get", url);
})();
(async (id) => {
const res1 = await ajax("get", url);
})();
}
# 3.2、代码演示
<script type="module">
import { ajax } from "./ajax.js";
async function foo() {
const url =
"https://www.fastmock.site/mock/6ec78e345df340241e1f5043f0167833/icode/users/list";
const arr = [1, 2, 3];
arr.forEach(async (id) => {
const res1 = await ajax("get", `${url}?num=${id}`);
console.log(res1);
});
}
foo();
</script>
# 4、判断以下代码输出的结果
// 休眠函数
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
async function foo() {
const arr = [6000, 1000, 4000];
arr.forEach(async (ms) => {
await sleep(ms);
console.log(ms);
await sleep(ms);
console.log(ms + "---");
});
}
foo();
# 五、async 与 await 在实际项目中的应用
TIP
深入浅出 async 与 await 在实际项目中的应用:页面加载进度条,动态加载二级菜单(await 与面向对象结合)
# 1、页面加载进度条
TIP
多张图片并行加载,用进度条显示图片加载的进度,如果全部加载完成进度条消失,图片显示出来。
# 1.1、HTML、CSS 布局
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
}
.progress {
width: 80%;
height: 50px;
}
.progress .progress-bar {
width: 0%;
height: 50px;
background-color: red;
text-align: center;
line-height: 50px;
color: #fff;
transition: all 0.2;
font-size: 30px;
}
</style>
<div class="progress">
<div class="progress-bar"></div>
</div>
<!-- 加载成功的图片放到这个div中 -->
<div class="imgContent"></div>
# 1.2、JS 实现思路
TIP
- 第一步:创建 loadImgAsync 方法,用来异步加载图片,返回值为 Promise 对象,图片加载成功,设 resolve 方法,把加载成功的图片传递过去,图片加载失败抛出错误。
- 第二步:创建加载进度条类
class Progress {}
,用来实现进度条效果。类上有两个方法。updata(loaded,total)
方法,用来更新进度条进度,loaded 表示当前完成量,total 表示需要完成的总量hide()
方法,用来隐藏进度条
- 第三步:创建 async 函数,来实现并行加载多张图片,在加载的过程中显示图片加载的进度,如果图片全部加载完成,隐藏进度条,显示图片(创建
render
函数来实现)。
// 加载图片方法
function loadImgAsync(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function () {
resolve(img);
};
img.onerror = function () {
reject("图片加载失败");
};
img.src = url;
});
}
// 进度条加载类
class Progress {
constructor(el) {
this.el = el; // 进度条DOM元素
}
// 更新进度条进度,loaded当前完成量,total需要完成的总量
updata(loaded, total) {
this.el.style.width = ((loaded / total) * 100).toFixed(0) + "%";
this.el.innerHTML = ((loaded / total) * 100).toFixed(0) + "%";
}
// 隐藏进度条
hide() {
this.el.parentNode.style.display = "none";
}
}
// 把加载成功的图片,添加到页面中对应的DOM元素内
function render(parentNode, imgs) {
for (let img of imgs) {
parentNode.appendChild(img);
}
}
// 异步并行加载多张图片
async function loadAllImg(urls) {
const imgArr = []; // 用来保存加载成功的图片
const total = imgUrls.length; // 需要加载的图片总数
let loaded = 0; // 当前加载的个数
// 创建进度条实例对象
const progress = new Progress(document.querySelector(".progress-bar"));
// 并行加载多张图片
imgUrls.forEach(async (url) => {
const img = await loadImgAsync(url);
// 将图片添加到数组中,统一保存起来
imgArr.push(img);
// 累计当前加载量
loaded++;
// 更新进度条
progress.updata(loaded, total);
// 判断图片是否加载完成,加载完成则隐藏进度条,同时将图片插入到页面中
if (loaded === total) {
// 隐藏进度
progress.hide();
// 将图片添加到页面
render(document.querySelector(".imgContent"), imgArr);
}
});
}
// 加载的图片
const imgUrls = [
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/08-29/210311f40bcf290736.jpg",
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/03-19/174949d70767470556.jpg",
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/02-19/16465934b475255075.jpg",
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2019/11-06/134028c28eb5212376.jpg",
];
loadAllImg(imgUrls);
# 2、动态加载二级菜单(await 与面向对象结合)
# 2.1、HTML、CSS 布局
TIP
创建index.html
页面,页面 html+css 内容如下
<style>
html,
body,
ul {
margin: 0;
padding: 0;
list-style: none;
}
.menu {
width: 200px;
margin-left: 300px;
margin-top: 100px;
position: relative;
}
.menu ul {
border: 1px solid #ddd;
}
.menu ul li {
padding-left: 20px;
height: 50px;
line-height: 50px;
cursor: pointer;
}
.menu ul li:hover {
background-color: tomato;
color: #fff;
}
.menu ul li:hover .content {
display: block;
}
.menu .content {
width: 200px;
min-height: 250px;
position: absolute;
left: 200px;
top: 0;
background-color: #ddd;
display: none;
padding: 0 10px;
}
.menu .content p {
display: flex;
align-items: center;
}
.menu .content p img {
width: 50px;
margin-right: 10px;
}
.menu .content p a {
text-decoration: none;
color: #000;
}
</style>
<body>
<div class="menu">
<!--
<ul>
<li data-id="1001" data-done="true">
人气 TOP
<div class="content">
<img src="./loading-svg/loading-balls.svg" alt="loding加载" />
<p>
<img src="xxxxx" alt="">
<a href="">生酪拿铁</a>
</p>
</div>
</li>
</ul>
-->
</div>
</body>
布局分析
- 上面代码中
class=content
容器中最开始只有 loading 加载图片,当鼠标滑动后,开始发请求加载载内容,内容加载成功后,就把 loading 加载图片隐藏或移除。 <li>
标签上有两个自定义属性data-id
属性保存对应主菜单的id
,当鼠标骨动到 li 时,获取这个 id 来作为 Ajax 发送 get 请求的参数,向后台获取对应的二级菜单内容。data-done
属性用来标识是否需要再次发起请求获取数据,渲染 DOM。一开始 li 上没有这个属性,所以获取 data-done 的值为 undefined,if 判断为 false,则需要发请求来获取数据并渲染 DOM,渲染后就给 li 添加了data-done=true
- 当鼠标再次滑动时,获取
data-done
的值为 true,if 判断为真,则表示数据已经加载并渲染了,不需要再请求,所以数据只要加载一次,后面滑动到 li 上就不需要再发送请求了。
接下来就通过 JS 来实现上面的功能。
# 2.1、JS 实现思路
TIP
创建两个类,分别为 Menu 和 SubMenu 类 Menu 类用来创建一级菜单,SubMenu 类用来创建二级菜单。同时 SubMenu 类继承 Menu 类。
两个类有以下方法和属性
Menu 类
属性和方法 | 说明 |
---|---|
el 属性 | DOM 元素,主菜单添加到的容器 |
url 属性 | 主菜单需要的数据地址(用于 Ajax 请求获取主菜单数据) |
getData 方法 | 获取主菜单的数据 (Ajax 请求后返回的数据) |
render 方法 | 用于把获取的数据渲染到页面(创建真实 DOM) |
SubMenu 类
属性和方法 | 说明 |
---|---|
el 属性 | 继承父类 |
url 属性 | 继承父类 |
getData 方法 | 继承父类 |
render 方法 | 重写,子类自己实现一份这个方法 |
needGetData | 判断是否需要获取数据,渲染 DOM,true 表示需要,false 表示不需要 html 结构中的 li 标签上的 data-done 属性,就是用来判断是否需要重新获取数据,渲染 DOM |
新建 Menu.js 文件
在 Menu.js
中创建 Menu 类和 SubMenu 类,并将两个类作为接口导出
import ajax from "./ajax.js";
class Menu {
constructor(el, url) {
this.el = el; // DOM元素
this.url = url; // 数据地址
}
// 获取数据
async getData() {
return await ajax("get", this.url);
}
// 渲染方法
async render() {
// 因为 getData的返回值是一个Promise对象,所以需要用await来取出对应的值
let data = (await this.getData()).data;
let html = "<ul>";
// 遍历数据,创建html标签
for (let item of data) {
html += `<li data-id=${item["category_id"]}>${item.title}
<div class="content">
<img src="./loading-svg/loading-balls.svg" alt="" />
</div>
</li>`;
}
html += "</ul>";
// 将内容添加到菜单容器中
this.el.innerHTML = html;
}
}
// 子菜单继承主菜单
class SubMenu extends Menu {
constructor(el, url) {
super(el, url);
}
// 原来的getData方法直接继承
// 重写render方法
async render() {
let data = (await this.getData()).data;
let html = "";
for (let { productName, productImg } of data) {
html += `<p>
<img src="${productImg}" />
<a href="">${productName}</a>
</p>`;
}
this.el.innerHTML = html;
// 将父元素的done属性值设为true,后面通过这个值判断是否需要通信加载数据渲染
this.el.parentNode.dataset.done = true;
}
// 是否需要获取数据,true需要,false不需要
needGetData() {
return !this.el.parentNode.dataset.done;
}
}
export { Menu, SubMenu };
ajax.js
文件
/**
* @param method 表示请求的方法,如get或post
* @param url 请求的地址
* @param body 如果为post请求,传入的请求体数据,需要传入JSON格式
*/
function ajax(method, url, body = null) {
// 返回Promise对象
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener("load", () => {
if (xhr.status === 200) {
resolve(xhr.response);
} else {
reject("请求失败");
}
});
// 响应过来的数据类型为json格式接受
xhr.responseType = "json";
xhr.open(method, url);
xhr.setRequestHeader("Content-Type", "application/json"); // 发送JSON格式数据
xhr.send(body);
});
}
export default ajax;
index.html
的 JS 代码如下
<script type="module">
import { Menu, SubMenu } from "./Menu.js";
const el = document.querySelector(".menu");
const url =
"https://www.fastmock.site/mock/6ec78e345df340241e1f5043f0167833/icode/menu";
// 创建一级主菜单
const menu = new Menu(el, url);
menu.render();
// 事件代理来处理,滑动加载二级菜单
el.addEventListener("mouseover", (e) => {
const target = e.target;
let url =
"https://www.fastmock.site/mock/6ec78e345df340241e1f5043f0167833/icode/menu/";
if (target.tagName.toLowerCase() !== "li") return;
// 创建二级菜单
const subMenu = new SubMenu(
target.querySelector(".content"),
`${url}${target.dataset.id}`
);
//加载数据,不过来要判断是否需要加载,如果需要就加载,不需要啥也不做
if (subMenu.needGetData()) {
// 开始渲染
subMenu.render();
}
});
</script>
# 2.2、与 fetch 结合
css 与之前一样,只是 JS 代码上有所不同
class Menu {
constructor(el, url) {
this.el = el; // 渲染出来的html元素要添加到那个DOM上
this.url = url; // 发请求获取数据的地址
}
async getData() {
const res = await fetch(this.url);
// -----------更简洁的写法,直接将结果作为返回值返回------
return (await res.json()).data;
// --------------------------------------------
}
async render() {
// -----------------等待结果------------------
const data = await this.getData();
// 拿到数据开始渲染
let html = "<ul>";
for (let item of data) {
html += `
<li data-id=${item["category_id"]} >
${item.title}
<div class="content">
<img src="./loading-svg/loading-balls.svg" alt="loding加载" />
</div>
</li>
`;
}
html += "</ul>";
// 将html添加到菜单容器中去
this.el.innerHTML = html;
}
}
class SubMenu extends Menu {
constructor(el, url) {
super(el, url);
}
// 重写render方法
async render() {
const data = await this.getData();
// 开始渲染
let html = "";
for (let { productName, productImg } of data) {
html += `
<p>
<img src="${productImg}" alt="" />
<a href="">${productName}</a>
</p>
`;
}
this.el.innerHTML = html;
// 父元素li身上添加对应的data-done属性
this.el.parentNode.dataset.done = true;
}
// 返回true和false,true需要加载,false表示不需要
needGetData() {
return !this.el.parentNode.dataset.done;
}
}
export { Menu, SubMenu };
<script type="module">
import { SubMenu, Menu } from "./Menu.js";
const el = document.querySelector(".menu");
const url =
"https://www.fastmock.site/mock/6ec78e345df340241e1f5043f0167833/icode/menu";
// 创建一级主菜单
const menu = new Menu(el, url);
menu.render();
// 利用事件代理,把mouseover事件由menu来代理
el.addEventListener("mouseover", (e) => {
const target = e.target;
if (target.tagName.toLowerCase() !== "li") return;
let url =
"https://www.fastmock.site/mock/6ec78e345df340241e1f5043f0167833/icode/menu/";
// 创建对应的二级菜单
const subMenu = new SubMenu(
target.querySelector(".content"),
`${url}${target.dataset.id}`
);
// 判断是否需要加载数据
if (subMenu.needGetData()) {
subMenu.render();
}
});
</script>
# 六、为什么需要 async 和 await
TIP
通过前面的学习,我们知道 JS 中解决异步编程的方案有很多种,为什么还需要 async 和 await 呢?要解决这个疑问,我们就需要了解每种异步编程解决方案之间的优缺点。
以下是常见的异步编程解决方案
- 回调函数
- Promise
- Generator
- async 与 await
接下来我们通过《异步继发加载多张图片》的案例来学习上面 4 种异地步解决方案的优缺点
# 1、回调函数实现:异步继发加载多张图片
TIP
继发加载多张图片,图片全部加载完后,再显示到页面中
/**
* loadImg 用来实现图片加载
* @param url图片加载地址
* @param resolve图片加载成功时的回调函数
* @param reject 图片加载失败时的回调函数
**/
function loadImg(url, resolve, reject) {
const img = new Image();
img.onload = function () {
resolve(img);
};
img.onerror = function () {
reject(`图片${url}加载失败`);
};
img.src = url;
}
// 继发加载3张图片
const urls = [
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/08-29/210311f40bcf290736.jpg",
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2020/02-08/145955bc3b00504448.jpg",
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/03-19/174949d70767470556.jpg",
];
// 继发加载多张图片
loadImg(urls[0], (img1) => {
loadImg(urls[1], (img2) => {
loadImg(urls[2], (img3) => {
// 三张图片加载成功,则将图片插入到页面中
document.body.appendChild(img1);
document.body.appendChild(img2);
document.body.appendChild(img3);
});
});
});
注:
通过上面的案例我们可以看到,如果只是加载 1 张图片,那回调函数的方式能很方便的帮我们实现。如果需要加载多张,就会出现层层嵌套的回调函数(回调地狱callback hell
)的问题也就出来。
如果需要加载的图片再多一些,那嵌套的级别会更深,不利于后期代码的维护,同时这种层层嵌套的写法也不符合正常代码的书写逻辑,而Promise
就可以解决这个问题。
# 2、Promise 实现:异步继发加载多张图片
/**
* loadImg 用来实现图片加载
* @param url图片加载地址
* @param resolve图片加载成功时的回调函数
* @param reject 图片加载失败时的回调函数
* */
function loadImg(url) {
// 返回值为Promise对象
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function () {
resolve(img);
};
img.onerror = function () {
reject(`图片${url}加载失败`);
};
img.src = url;
});
}
// 继发加载3张图片
const urls = [
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/08-29/210311f40bcf290736.jpg",
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2020/02-08/145955bc3b00504448.jpg",
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/03-19/174949d70767470556.jpg",
];
const arr = [];
loadImg(urls[0])
.then((img1) => {
arr.push(img1);
return loadImg(urls[1]);
})
.then((img2) => {
arr.push(img2);
return loadImg(urls[2]);
})
.then((img3) => {
arr.push(img3);
for (let img of arr) {
document.body.appendChild(img);
}
});
注:
Promise 改造后的代码是按正常的代码书写逻辑,从上往下来书写,同时解决了“回调地狱 callback hell”问题。他使的异步操作能以同步操作的流程表达出来。
但是过多的 then 和回调使代码看起来并不是那么的简洁。而 Generator 可以解决这个问题。
# 3、Generator 实现:异步继发加载多张图片
<script>
/**
*loadImg 用来实现图片加载
* @param url图片加载地址
* @param resolve图片加载成功时的回调函数
* @param reject 图片加载失败时的回调函数
* */
function loadImg(url) {
// 返回值为Promise对象
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function () {
resolve(img);
};
img.onerror = function () {
reject(`图片${url}加载失败`);
};
img.src = url;
});
}
// 继发加载3张图片
const urls = [
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/08-29/210311f40bcf290736.jpg",
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2020/02-08/145955bc3b00504448.jpg",
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/03-19/174949d70767470556.jpg",
];
// Generator函数
function* gen() {
const img1 = yield loadImg(urls[0]);
const img2 = yield loadImg(urls[1]);
const img3 = yield loadImg(urls[2]);
document.body.appendChild(img1);
document.body.appendChild(img2);
document.body.appendChild(img3);
}
// 生成迭代器对象
const it = gen();
// 手动调用next方法来执行代码
it.next().value.then((img) => {
it.next(img).value.then((img) => {
it.next(img).value.then((img) => {
it.next(img);
});
});
});
</script>
通过上面代码,可以看出 Generator 的写法使得异步的代码可以完全像同步代码一样书写,并且简洁明了。
唯一的缺点:
就是需要人为的调用next
方法来手动执行代码,这一点相当的不友好。为了解决这个问题,我们必需手动书写执行器函数,来执行 Generator 函数体中的代码,所以出现了 co 模块来解决 Generator 函数自执行的问题。
而 async 和 await 的出现,解决了这一问题,并且还做了其它的相关优化。
以下是我们为上面的 Generator 函数实现的一个简单的自执行器函数
// Generator函数的自执行器
function run(gen) {
const it = gen();
function next(data) {
const result = it.next(data);
// 如果result.done=true,表示内部代码执行完,不需要再执行
if (result.done) return result.value;
result.value.then((data) => {
next(data);
});
}
next();
}
// 调用自执行器函数
run(gen);
# 4、async 与 await 实现:异步继发加载多张图片
/**
* loadImg 用来实现图片加载
* @param url图片加载地址
* @param resolve图片加载成功时的回调函数
* @param reject 图片加载失败时的回调函数
* */
function loadImg(url) {
// 返回值为Promise对象
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function () {
resolve(img);
};
img.onerror = function () {
reject(`图片${url}加载失败`);
};
img.src = url;
});
}
// 继发加载3张图片
const urls = [
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/08-29/210311f40bcf290736.jpg",
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2020/02-08/145955bc3b00504448.jpg",
"https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/03-19/174949d70767470556.jpg",
];
async function gen() {
const img1 = await loadImg(urls[0]);
const img2 = await loadImg(urls[1]);
const img3 = await loadImg(urls[2]);
document.body.appendChild(img1);
document.body.appendChild(img2);
document.body.appendChild(img3);
}
gen();
注:
能过上面的代码我们可以看出,async 和 await 除了让异步代码可以相同步代码一样书写,而且代码也非常的简洁明了。也不需要我们手动书写自执行器函数,只要我们调下 async 函数,其内部就会自动执行。
async 和 await 其实就是 Generator 函数的语法糖。async 函数就是将 Generator 函数的星号
*
替换成了 async,将 yield 替换成了 await,同时自带了自执行器等.....
async 函数相对 Generator 函数而言
主要做了以下 4 方面的改进:
- 内置执行器: Generator 函数需要通过手动调用 next 方法才能执行函数体中的代码,所以我们需要人为的为其书写自执行器函数。而 async 和 await 就没有这个问题,只要调下 async 函数,内部代码就会自动执行,相当于内置了执行器。
- 更好的语议: async 和 await 比起
*
星号和yield
,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等结果 - 更广的适用性: Generator 函数如果与 Co 模块结合完成自执行,其 yield 后面只能是 Thunk 函数或 Promise 对象,async 函数的 await 命令后面,可以是 Promise 的对象,也可以是原始类型的值(数值、字符串和布尔值,但这时等同于同步操作。
- 返回值是 Promise:async 函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便了许多,可以用 then 方法指定下一步的操作。
进一步说,async 函数完全可以看作由多个异步操作包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖。
# 七、总结
TIP
总结本章重难点知识,理清思路,把握重难点。并能轻松回答以下问题,说明自己就真正的掌握了。
用于故而知新,快速复习。
# 1、async/await 是什么
TIP
- async/await 是 ES2017 新增的关键字
- async 函数是使用 async 关键字声明的函数
- 使基于 Promise 的异步操作更简洁、更方便
- 使异步代码看起来像同步的,更容易理解
// 回调函数
loadImg(urls[0], (img1) => {
loadImg(urls[1], (img2) => {
loadImg(urls[2], (img3) => {
// .....
});
});
});
// Promise
loadImg(urls[0])
.then((img1) => {
return loadImg(urls[1]);
})
.then((img2) => {
return loadImg(urls[2]);
})
.then((img3) => {});
// Generator函数
function* gen() {
const img1 = yield loadImg(urls[0]);
const img2 = yield loadImg(urls[1]);
const img3 = yield loadImg(urls[2]);
// ....
}
// Generator函数的自执行器
function run(gen) {
// ....
}
// async与await
async function gen() {
const img1 = await loadImg(urls[0]);
const img2 = await loadImg(urls[1]);
const img3 = await loadImg(urls[2]);
// ....
}
注:
代码由之前的回调地狱嵌套的形式 -> 发展成 Promise 的 then 链形式 -> Generator 的同步形式---> 发展到 async/await 形式,变得更简单、更方便、更容易理解
# 2、async 函数
TIP
- async 函数的返回值是 Promise 对象
- return 后面的值,如果是 Promise,直接返回该 Promise 对象
- 如果不是 Promise ,相当于包了一层
Promise.resolve()
再返回
// 如果 return 后面的值不是 Promise ,相当于包了一层 Promise.resolve() 再返回
async function foo() {
// return 123;
// 相当于
return Promise.resolve(123);
}
// return 后面的值,如果是 Promise,直接返回该 Promise 对象
async function fn() {
// 直接返回该 Promise 对象
return new Promise((resolve) => {
setTimeout(() => {
resolve(123);
}, 1000);
});
}
# 3、async 函数的各种写法
TIP
- 函数声明
- 函数表达式
- 箭头函数
- 对象的方法
- Class 的方法
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
// 箭头函数
const foo = async () => {};
const foo = async (param) => {};
// 对象的方法
const obj = {
foo: async function () {},
foo: async () => {},
async foo() {},
};
// Class 的方法
class Person {
async foo() {}
}
# 4、async 函数在实际开发中的注意事项
TIP
- 可以通过
try ... catch
或Promise ... catch
的方式来处理错误 - async 函数中可以没有 await
# 5、await 的用法
TIP
- async 函数内部是同步执行的,它本身是异步的
- 如果 await 后面是一个 Promise,值就是该 Promise 对象的结果
- 如果 await 后面不是 Promise,await 的值就是该值
(async () => {
// 并发
// async 函数本身是异步的
imgUrls.forEach(async (url) => {
const img = await loadImgAsync(url);
});
})();
# 6、await 的值
TIP
- 如果 await 后面是一个 Promise 对象,await 的值就是该 Promise 对象的结果(PromiseResult)
- 如果 await 后面不是 Promise 对象,await 的值就是该值,相当于包了一层
Promise.resolve()
之后在获取该 Promise 对象的结果
// 值:123
await Promise.resolve(123);
// 值:123,相当于 await Promise.resolve(123)
await 123;
# 7、await 在实际开发中的注意事项
TIP
- 所有 await 都成功,async 函数返回的 Promise 对象才会成功
- 只要任何一个 await 失败,async 函数返回的 Promise 对象就失败
- await 一般用在 async 函数中,async 函数中可以没有 await
async function ad() {
await delayed(1000);
// 显示广告
await delayed(2000);
// 隐藏广告
await delayed(1000);
// 显示广告
await delayed(2000);
// 隐藏广告
}
注:
以上代码中,async 函数中有很多 await ,只要其中任何一个 await 出错了,我们这个 async 函数它返回的 Promise 对象就会出错。
只有所有的 await 都成功了,那么 async 函数返回的 Promise 对象才能成功。
# 8、继发和并发
TIP
- 处理异步操作时,如果不存在继发关系,最好让它并发执行
- 比如:我们要发送一个 Ajax 请求,如果这两次请求之前没有明确的先后发送请求的要求,最好就让它们并发执行,这样是最有效率的。
- 对于继发问题,是非常容易处理的,我们只需要在 async 函数中一步步使用 await 来处理异步操作,它就是继发的
并发问题,主要有两种解决方法
- 可以先执行异步操作,再 await 等待结果
- 也可以通过
Promise.all
让异步操作并发执行
// 方法一:先执行异步操作,再 await 等待结果
const jsPromise = getJSON(`${url}js`);
const jsonPromise = getJSON(`${url}json`);
const jsResult = await jsPromise;
console.log(jsResult);
const jsonResult = await jsonPromise;
console.log(jsonResult);
// 方法二:Promise.all 让异步操作并发执行
const datas = await Promise.all([getJSON(`${url}js`), getJSON(`${url}json`)]);
console.log(datas);
大厂最新技术学习分享群
微信扫一扫进群,获取资料
X