# 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);

image-20230309225349650

# 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);
});

image-20230309211514026

# 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("最后执行我");
});

GIF2023-3-922-36-02

# 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 ... catchPromise ... 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();

image-20230310183848390

# 五、async 与 await 在实际项目中的应用

TIP

深入浅出 async 与 await 在实际项目中的应用:页面加载进度条,动态加载二级菜单(await 与面向对象结合)

# 1、页面加载进度条

TIP

多张图片并行加载,用进度条显示图片加载的进度,如果全部加载完成进度条消失,图片显示出来。

GIF2023-3-1018-45-19

# 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 与面向对象结合)

GIF2023-3-111-35-23

# 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 ... catchPromise ... 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);
上次更新时间: 6/8/2023, 9:23:17 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X