Skip to content

AJAX 进阶

知识点自测

  1. 看如下标签回答如下问题?

    html
    <select>
      <option value="北京">北京市</option>
      <option value="南京">南京市</option>
      <option value="天津">天津市</option>
    </select>
    • 当选中第二个 option 时,JS 中获取下拉菜单 select 标签的 value 属性的值是多少?

      答案
      • 南京。当选中第二个 option 时,JS 中获取下拉菜单 select 标签的 value 属性的值是 "南京"
      • 这是因为 value 属性的值是当前选中的 option 元素的 value 属性值。
      • 在这个例子中,第二个 optionvalue 属性是 "南京"
    • 页面上看到的是北京,还是北京市?

      答案
      • 北京市。页面上显示的文本是由选中的 option 元素的文本内容决定的。
      • 在这个例子中,当选中第一个 option 时,页面上显示的是 北京市。所以,页面上看到的是 北京市
    • 我给 select 标签的 value 属性赋予 "南京" 会有什么效果?

      答案
      • 什么效果都没有,没有没有一个 option 选项的 value 能匹配。
      • select 标签的 value 属性赋予 "南京" 不会产生直观的效果,因为 value 属性通常是由当前选中的 option 决定的。

学习目标

  1. 区分异步代码,回调函数地狱问题和所有解决防范(Promise 链式调用)
  2. 掌握 asyncawait 使用
  3. 掌握 EventLoop 的概念
  4. 了解 Promise.all 静态方法作用
  5. 完成省市区切换效果

同步代码和异步代码

  1. 同步代码:逐行执行,需原地等待结果后,才继续向下执行
  2. 异步代码:调用后耗时,不阻塞代码继续执行(不必原地等待),在将来完成后触发回调函数传递结果

小结

  1. 什么是同步代码?

    • 逐行执行,原地等待结果后,才继续向下执行
  2. 什么是异步代码?

    • 调用后耗时,不阻塞代码执行,将来完成后触发回调函数
  3. JS 中有哪些异步代码?

    • setTimeout / setInterval
    • 事件
    • AJAX
  4. 异步代码如何接收结果?

    • 依靠回调函数来接收

回调函数地狱

  1. 概念:在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱
  2. 缺点:可读性差,异常无法捕获,耦合性严重,牵一发动全身
示例 - 回调函数地狱
  • 需求:展示默认第一个省,第一个城市,第一个地区在下拉菜单中
js
axios({ url: 'http://hmajax.itheima.net/api/province' }).then((result) => {
  const pname = result.data.list[0];
  document.querySelector('.province').innerHTML = pname;
  // 获取第一个省份默认下属的第一个城市名字
  axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } }).then((result) => {
    const cname = result.data.list[0];
    document.querySelector('.city').innerHTML = cname;
    // 获取第一个城市默认下属第一个地区名字
    axios({ url: 'http://hmajax.itheima.net/api/area', params: { pname, cname } }).then((result) => {
      document.querySelector('.area').innerHTML = result.data.list[0];
    });
  });
});

小结

  1. 什么是回调函数地狱?

    • 在回调函数一直向下嵌套回调函数,形成回调函数地狱
  2. 回调函数地狱问题?

    • 可读性差
    • 异常捕获困难
    • 耦合性严重

Promise-链式调用

  1. 概念:依靠 then() 方法会返回一个新生成的 Promise 对象特性,继续串联下一环任务,直到结束
  2. 细节:then() 回调函数中的返回值,会影响新生成的 Promise 对象最终状态和结果
  3. 好处:通过链式调用,解决回调函数嵌套问题

357e8cbd-ac70-4b06-9db1-cd300505bd8e

js
/**
 * 目标:掌握 Promise 的链式调用
 * 需求:把省市的嵌套结构,改成链式调用的线性结构
 */
// 1. 创建 Promise 对象 - 模拟请求省份名字
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('北京市');
  }, 2000);
});

// 2. 获取省份名字
const p2 = p.then((result) => {
  console.log(result);
  // 3. 创建 Promise 对象 - 模拟请求城市名字
  // return Promise 对象最终状态和结果,影响到新的 Promise 对象
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(result + '--- 北京');
    }, 2000);
  });
});

// 4. 获取城市名字
p2.then((result) => {
  console.log(result);
});

// then() 原地的结果是一个新的 Promise 对象
console.log(p2 === p);

解决回调地狱

  1. 目标:使用 Promise 链式调用,解决回调函数地狱问题
  2. 做法:每个 Promise 对象中管理一个异步任务,用 then 返回 Promise 对象,串联起来

bf54567c-df9a-4ca2-9edc-50047617bd81

示例 - Promise 链式调用解决回调函数
html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>33 Promise 链式调用解决回调地狱 获取地方列表</title>
  <link rel="stylesheet" href="./css/bootstrap.min.css">

</head>

<body class="p-3">
  <div class="container">
    <h2>33 Promise 链式调用解决回调地狱 获取地方列表</h2>
    <form class="form-group row my-5 " id="editForm">
      <div class="mb-3 col">
        <label for="province" class="form-label">省份</label>
        <select class="form-select" id="province" name="province">
          <option value="北京" selected>北京</option>
        </select>
      </div>
      <div class="mb-3 col">
        <label for="city" class="form-label">城市</label>
        <select class="form-select" id="city" name="city">
          <option value="北京市" selected>北京市</option>
        </select>
      </div>
      <div class="mb-3 col">
        <label for="area" class="form-label">地区</label>
        <select class="form-select" id="area" name="area">
          <option value="东城区" selected>东城区</option>
        </select>
      </div>
    </form>
  </div>
  <!-- <script src="./index.js"></script> -->
  <script src="./js/axios.min.js"></script>
  <script>
    /**
     * 目标:把回调函数嵌套代码,改成 Promise 链式调用结构
     * 需求:获取默认第一个省,第一个市,第一个地区并展示在下拉菜单中
     */
    const province = document.querySelector('#province');
    const city = document.querySelector('#city');
    const area = document.querySelector('#area');

    axios
      .get('http://hmajax.itheima.net/api/province')
      .then((res) => {
        console.log(res.data);
        // province.innerHTML = res.data.list.map(item => `
        //   <option value="${item}">${item}</option>
        // `).join('')
        province.innerHTML = `<option value="${res.data.list[6]}">${res.data.list[6]}</option>`;
        return axios.get(`http://hmajax.itheima.net/api/city?pname=${province.value}`);
      })
      .then((res) => {
        console.log(res.data);
        // city.innerHTML = res.data.list.map(item => `
        //   <option value="${item}">${item}</option>
        // `).join('')
        city.innerHTML = `<option value="${res.data.list[6]}">${res.data.list[6]}</option>`;
        return axios.get(`http://hmajax.itheima.net/api/area?pname=${province.value}&cname=${city.value}`);
      })
      .then((res) => {
        console.log(res.data);
        area.innerHTML = res.data.list.map((item) => `<option value="${item}">${item}</option>`).join('');
      });

  </script>
</body>

</html>

小结

  1. 什么是 Promise 的链式调用?

    • 使用 then 方法返回新 Promise 对象特性,一直串联下去
  2. then 回调函数中,return 的值会传给哪里?

    • 传给 then 方法生成的新 Promise 对象
  3. Promise 链式调用有什么用?

    • 解决回调函数嵌套问题
  4. Promise 链式调用如何解决回调函数地狱?

    • then 的回调函数中返回 Promise 对象,影响当前新 Promise 对象的值

async 函数和 await 捕获错误

  1. 概念:在 async 函数内,使用 await 关键字取代 then 函数,等待获取 Promise 对象成功状态的结果值
  2. 做法:使用 async 和 await 解决回调地狱问题
示例 - async 函数和 await 解决回调函数地狱
html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>34 async 函数和 await 解决回调函数地狱 获取地方列表</title>
  <link rel="stylesheet" href="./css/bootstrap.min.css">

</head>

<body class="p-3">
  <div class="container">
    <h2>34 async 函数和 await 解决回调函数地狱 获取地方列表</h2>
    <form class="form-group row my-5 " id="editForm">
      <div class="mb-3 col">
        <label for="province" class="form-label">省份</label>
        <select class="form-select" id="province" name="province">
          <option value="北京" selected>北京</option>
        </select>
      </div>
      <div class="mb-3 col">
        <label for="city" class="form-label">城市</label>
        <select class="form-select" id="city" name="city">
          <option value="北京市" selected>北京市</option>
        </select>
      </div>
      <div class="mb-3 col">
        <label for="area" class="form-label">地区</label>
        <select class="form-select" id="area" name="area">
          <option value="东城区" selected>东城区</option>
        </select>
      </div>
    </form>
  </div>
  <!-- <script src="./index.js"></script> -->
  <script src="./js/axios.min.js"></script>
  <script>
    /**
     * 目标:掌握 async 和 await 语法,解决回调函数地狱
     * 概念:在 async 函数内,使用 await 关键字,获取 Promise 对象"成功状态"结果值
     * 注意:await 必须用在 async 修饰的函数内(await 会阻止"异步函数内"代码继续执行,原地等待结果)
     */
    const province = document.querySelector('#province');
    const city = document.querySelector('#city');
    const area = document.querySelector('#area');

    // 1. 定义 async 修饰函数
    async function getData() {
      // 2. 在 async 函数内,使用 await 关键字,获取 Promise 对象"成功状态"结果值
      const res1 = await axios.get('http://hmajax.itheima.net/api/province');
      console.log(res1.data);
      province.innerHTML = `<option value="${res1.data.list[10]}">${res1.data.list[10]}</option>`;

      const res2 = await axios.get(`http://hmajax.itheima.net/api/city?pname=${province.value}`);
      console.log(res2.data);
      city.innerHTML = `<option value="${res2.data.list[10]}">${res2.data.list[10]}</option>`;

      const res3 = await axios.get(`http://hmajax.itheima.net/api/area?pname=${province.value}&cname=${city.value}`);
      console.log(res3.data);
      area.innerHTML = res3.data.list.map((item) => `<option value="${item}">${item}</option>`).join('');
    }

    getData();
  </script>
</body>

</html>

try catch 捕获同步流程的错误

  1. try 和 catch 的作用:语句标记要尝试的语句块,并指定一个出现异常时抛出的响应

    js
    try {
      // 要执行的代码
    } catch (error) {
      // error 接收的是,错误消息
      // try 里代码,如果有错误,直接进入这里执行
    }

    try 里有报错的代码,会立刻跳转到 catch 中

  2. 尝试把代码中 url 地址写错,运行观察 try catch 的捕获错误信息能力

try catch 捕获同步流程的错误
html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>35 async 函数和 await 错误捕获 获取地方列表</title>
  <link rel="stylesheet" href="./css/bootstrap.min.css">

</head>

<body class="p-3">
  <div class="container">
    <h2>35 async 函数和 await 错误捕获 获取地方列表</h2>
    <form class="form-group row my-5 " id="editForm">
      <div class="mb-3 col">
        <label for="province" class="form-label">省份</label>
        <select class="form-select" id="province" name="province">
          <option value="北京" selected>北京</option>
        </select>
      </div>
      <div class="mb-3 col">
        <label for="city" class="form-label">城市</label>
        <select class="form-select" id="city" name="city">
          <option value="北京市" selected>北京市</option>
        </select>
      </div>
      <div class="mb-3 col">
        <label for="area" class="form-label">地区</label>
        <select class="form-select" id="area" name="area">
          <option value="东城区" selected>东城区</option>
        </select>
      </div>
    </form>
  </div>
  <!-- <script src="./index.js"></script> -->
  <script src="./js/axios.min.js"></script>
  <script>
    /**
     * 目标:async 和 await 错误捕获
     */
    const province = document.querySelector('#province');
    const city = document.querySelector('#city');
    const area = document.querySelector('#area');

    async function getData() {
      // 1. try 包裹可能产生错误的代码
      try {
        const res1 = await axios.get('http://hmajax.itheima.net/api/province');
        console.log(res1.data);
        province.innerHTML = `<option value="${res1.data.list[10]}">${res1.data.list[10]}</option>`;

        const res2 = await axios.get(`http://hmajax.itheima.net/api/city?pname=${province.value}`);
        console.log(res2.data);
        city.innerHTML = `<option value="${res2.data.list[10]}">${res2.data.list[10]}</option>`;

        // const res3 = await axios.get(`http://hmajax.itheima.net/api/area?pname=${province.value}&cname=${city.value}`);
        const res3 = await axios.get(`http://hmajax.itheima.net/api/area1000?pname=${province.value}&cname=${city.value}`);
        console.log(res3.data);
        area.innerHTML = res3.data.list.map((item) => `<option value="${item}">${item}</option>`).join('');
      } catch (err) {
        // 2. 接着调用 catch 块,接收错误信息
        // 如果 try 里某行代码报错后,try 中剩余的代码不会执行了
        console.dir(error);
      }
    }

    getData();

  </script>
</body>

</html>

小结

  1. await 的作用是什么?

    • 替代 then 方法来提取 Promise 对象成功状态的结果
  2. trycatch 有什么作用?

    • 捕获同步流程的代码报错信息

事件循环

  1. 事件循环(EventLoop):掌握后知道 JS 是如何安排和运行代码
  2. 作用:事件循环负责执行代码,收集和处理事件以及执行队列中的子任务
  3. 原因:JavaScript 单线程(某一刻只能执行一行代码),为了让耗时代码不阻塞其他代码运行,设计了事件循环模型
  4. 概念:执行代码和收集异步任务的模型,在调用栈空闲,反复调用任务队列里回调函数的执行机制,就叫事件循环

5af81dfb-ba2d-4ffe-9715-32d957b22454

事件循环 - 练习

  • 请根据掌握的事件循环的模型概念,分析代码执行过程
事件循环 - 练习
html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>36 事件循环练习</title>
</head>

<body>
  <script>
    console.log(1); // 进入调用栈,执行同步代码,然后出栈,打印 1
    setTimeout(() => { // 宿主环境(浏览器),0s 后将回调函数放入任务队列
      console.log(2);
    }, 0);
    function myFn() {
      console.log(3);
    }
    function ajaxFn() {
      const xhr = new XMLHttpRequest(); // ajax 异步请求,将回调函数放入任务队列
      xhr.open('GET', 'http://hmajax.itheima.net/api/province');
      xhr.addEventListener('loadend', () => {
        console.log(4);
      });
      xhr.send();
    }
    for (let i = 0; i < 1; i++) { // for 循环,执行同步代码,然后出栈,打印 5
      console.log(5);
    }
    ajaxFn(); // ajaxFn 函数,执行同步代码,然后出栈,打印 4
    document.addEventListener('click', () => { // 事件监听,用户点击后输出 6
      console.log(6);
    });
    myFn(); // myFn 函数,执行同步代码,然后出栈,打印 3

    // 调用栈:
    // 1. console.log(1);
    // 2. for (let i = 0; i < 1; i++) { console.log(5); }
    // 3. myFn(); function myFn() { console.log(3); }

    // 任务队列:
    // 1. setTimeout(() => { console.log(2); }, 0);

    // 宿主环境(浏览器):
    // 1. xhr.addEventListener('loadend', () => { console.log(4); });
    // 2. document.addEventListener('click', () => { console.log(6); });

    // 打印结果:
    // 1 5 3 2 4(用户点击后)6
  </script>
</body>

</html>

0dce1a51-773c-456b-bc4f-0f6c47b712a0

宏任务与微任务

  1. ES6 之后引入了 Promise 对象,让 JS 引擎也可以发起异步任务

  2. 异步任务划分为了

    • 宏任务:由浏览器环境执行的异步代码
    • 微任务:由 JS 引擎环境执行的异步代码
  3. 宏任务和微任务具体划分:

    deffa49c-44d1-4c22-9bc9-945ddcb2411a

  4. 事件循环模型

    js
    /**
     * 目标:阅读并回答打印的执行顺序
     */
    console.log(1);
    setTimeout(() => {
      console.log(2);
    }, 0);
    const p = new Promise((resolve, reject) => {
      resolve(3);
    });
    p.then((res) => {
      console.log(res);
    });
    console.log(4);

    97be932c-443e-42d1-b434-d8bc8f220ac2

    注意:宏任务每次在执行同步代码时,产生微任务队列,清空微任务队列任务后,微任务队列空间释放!

    下一次宏任务执行时,遇到微任务代码,才会再次申请微任务队列空间放入回调函数消息排队

    总结:一个宏任务包含微任务队列,他们之间是包含关系,不是并列关系

事件循环 - 经典面试题

  • 需求:请切换到对应配套代码,查看具体代码,并回答打印顺序(锻炼事件循环概念的理解,阅读代码执行顺序)
事件循环 - 经典面试题
html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>37 事件循环面试题</title>
</head>

<body>
  <script>
    console.log(1); // 进入调用栈,执行同步代码
    setTimeout(() => { // 宿主环境(浏览器),0s 后将回调函数放入任务队列
      console.log(2);
      const p = new Promise((resolve) => resolve(3)); // Promise 构造函数,执行同步代码
      p.then((result) => console.log(result)); // Promise.then 回调函数,执行异步代码
    }, 0);
    const p = new Promise((resolve) => { // Promise 构造函数,执行同步代码
      setTimeout(() => { // 宿主环境(浏览器),0s 后将回调函数放入任务队列
        console.log(4);
      }, 0);
      resolve(5); // Promise.resolve 执行同步代码
    });
    p.then((result) => console.log(result)); // Promise.then 回调函数,执行异步代码
    const p2 = new Promise((resolve) => resolve(6)); // Promise 构造函数,执行同步代码
    p2.then((result) => console.log(result)); // Promise.then 回调函数,执行异步代码
    console.log(7); // 进入调用栈,执行同步代码

    // 调用栈:
    // 1. console.log(1);
    // 2. console.log(7);

    // 宿主环境(浏览器):
    // 1. setTimeout(() => { console.log(2); const p = new Promise((resolve) => resolve(3)); p.then((result) => console.log(result)); }, 0);
    // 2. setTimeout(() => { console.log(4); }, 0); }

    // 任务队列 - 微任务:
    // 1. const p = new Promise((resolve) => { resolve(5); }).then((result) => console.log(result));
    // 2. const p2 = new Promise((resolve) => resolve(6)).then((result) => console.log(result));

    // 任务队列 - 宏任务:
    // 1. console.log(2); const p = new Promise((resolve) => resolve(3)); p.then((result) => console.log(result));
    // 2. console.log(4);

    // 打印结果:
    // 1 7 5 6 2 3 4
  </script>
</body>

</html>

496bb0b6-5c5c-45c1-8937-f769dd833052

小结

  1. 什么是事件循环?

    • 执行代码和收集异步任务,在调用栈空闲时,反复调用任务队列里回调函数执行机制
  2. 为什么有事件循环?

    • JavaScript 是单线程的,为了不阻塞 JS 引擎,设计执行代码的模型
  3. JavaScript 内代码如何执行?

    • 执行同步代码,遇到异步代码交给宿主浏览器环境执行
    • 异步有了结果后,把回调函数放入任务队列排队
    • 当调用栈空闲后,反复调用任务队列里的回调函数
  4. 什么是宏任务?

    • 浏览器执行的异步代码
    • 例如:JS 执行脚本事件,setTimeout/setInterval,AJAX 请求完成事件,用户交互事件等
  5. 什么是微任务?

    • JS 引擎执行的异步代码
    • 例如:Promise 对象.then() 的回调
  6. JavaScript 内代码如何执行?

    • 执行第一个 script 脚本事件宏任务,里面同步代码
    • 遇到 宏任务/微任务 交给宿主环境,有结果回调函数进入对应队列
    • 当执行栈空闲时,清空微任务队列,再执行下一个宏任务,从 1 再来

    7b223efb-1149-4a39-ba98-892920bb0d8f

Promise.all 静态方法

  1. 概念:合并多个 Promise 对象,等待所有同时成功完成(或某一个失败),做后续逻辑

    690be3de-f256-4e8c-a360-999fed6bdf1b

  2. 语法:

    js
    const p = Promise.all([Promise 对象Promise 对象...])
    p.then(result => {
      // result 结果:[Promise 对象成功结果,Promise 对象成功结果,...]
    }).catch(error => {
      // 第一个失败的 Promise 对象,抛出的异常对象
    })

案例 - 同时显示多地天气

案例 - 同时显示多地天气
html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>38 同时显示多地天气 - Promise.all</title>
  <link rel="stylesheet" href="./css/bootstrap.min.css">
</head>

<body class="p-3">
  <div class="container">
    <h2>38 同时显示多地天气 - Promise.all</h2>
    <ul class="list-group my-5">
      <li class="list-group-item">...</li>
    </ul>
  </div>

  <script src="./js/axios.min.js"></script>
  <script>
    /**
     * 目标:掌握 Promise 的 all 方法作用,和使用场景
     * 业务:当我需要同一时间显示多个请求的结果时,就要把多请求合并
     * 例如:默认显示"北京", "上海", "广州", "深圳"的天气在首页查看
     * code:
     *  北京 (110100) 上海 (310100) 广州 (440100) 深圳 (440300)
     */

    // 1. 请求城市天气,得到 Promise 对象
    // const bjPromise = axios({ url: 'http://hmajax.itheima.net/api/weather', params: { city: '110100' } })
    // const shPromise = axios({ url: 'http://hmajax.itheima.net/api/weather', params: { city: '310100' } })
    // const gzPromise = axios({ url: 'http://hmajax.itheima.net/api/weather', params: { city: '440100' } })
    // const szPromise = axios({ url: 'http://hmajax.itheima.net/api/weather', params: { city: '440300' } })
    const getWeather = (cityCode) => {
      return axios.get(`http://hmajax.itheima.net/api/weather?city=${cityCode}`);
    };
    const cityCodes = ['110100', '310100', '440100', '440300'];
    const cityPromises = cityCodes.map((cityCode) => getWeather(cityCode));

    // 2. 合并多个 Promise 对象,得到一个新的 Promise 对象
    const allPromise = Promise.all(cityPromises);

    // 3. 处理请求结果
    allPromise
      .then((res) => {
        console.log(res);
        document.querySelector('.list-group').innerHTML = res
          .map((item) => {
            return `<li class="list-group-item">【${item.data.data.area}】天气:${item.data.data.weather}</li>`;
          })
          .join('');
      })
      .catch((err) => {
        console.dir(err);
      });
  </script>
</body>

</html>

案例 - 商品分类导航

目标:把所有商品分类 "同时" 渲染到页面上

  1. 获取所有一级分类数据
  2. 遍历 id,创建获取二级分类请求
  3. 合并所有二级分类 Promise 对象
  4. 等待同时成功后,渲染页面
案例 - 商品分类导航
html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>39 商品分类导航</title>
  <link rel="stylesheet" href="./css/index.css">
</head>

<body>
  <!-- 大容器 -->
  <div class="container">
    <div class="sub-list">
      <div class="item">
        <h3>分类名字</h3>
        <ul>
          <li>
            <a href="#"><img src="http://zhoushugang.gitee.io/erabbit-client-pc-static/uploads/img/category%20(9).png"
                referrerpolicy="no-referrer" />
              <p>巧克力</p>
            </a>
          </li>
        </ul>
      </div>
    </div>
  </div>
  <script src="./js/axios.min.js"></script>
  <script src="./js/index.js"></script>
</body>

</html>
js
/**
 * 目标:把所有商品分类“同时”渲染到页面上
 *  1. 获取所有一级分类数据
 *  2. 遍历 id,创建获取二级分类请求
 *  3. 合并所有二级分类 Promise 对象
 *  4. 等待同时成功后,渲染页面
 */

// 1. 获取所有一级分类数据
// api: http://hmajax.itheima.net/api/category/top - GET
axios.get('http://hmajax.itheima.net/api/category/top').then((res) => {
  console.log(res);

  // 2. 遍历 id,创建获取二级分类请求
  // api: http://hmajax.itheima.net/api/category/sub/:id - GET
  const subCategoryPromises = res.data.data.map((item) => {
    return axios.get(`http://hmajax.itheima.net/api/category/sub?id=${item.id}`);
  });
  console.log(subCategoryPromises); // [Promise, Promise, Promise, Promise]

  // 3. 合并所有二级分类 Promise 对象
  Promise.all(subCategoryPromises).then((res) => {
    console.log(res); // res: [Array, Array, Array, Array]

    // 4. 等待同时成功后,渲染页面
    document.querySelector('.sub-list').innerHTML = res
      .map((item) => {
        const category = item.data.data;
        return `
            <div class="item">
              <h3>${category.name}</h3>
              <ul>
                ${category.children
                  .map((item) => {
                    return `<li><a href="#"><img src="${item.picture}" referrerpolicy="no-referrer" /><p>${item.name}</p></a></li>`;
                  })
                  .join('')}
              </ul>
            </div>
          `;
      })
      .join('');
  });
});

小结

  1. Promise.all 什么时候使用?

    • 合并多个 Promise 对象并等待所有同时成功的结果,如果有一个报错就会最终为失败状态,当需要同时渲染多个接口数据同时到网页上时使用

今日重点

  1. 掌握 async 和 await 的使用

    • async 函数是用来定义一个返回 Promise 对象的函数。在函数体内部,可以使用 await 来暂停函数的执行,等待 Promise 对象的解决。

      js
      async function exampleAsyncFunction() {
        console.log('Start');
      
        // 使用 await 暂停函数执行,等待 Promise 对象解决
        const result = await new Promise((resolve) => {
          setTimeout(() => {
            resolve('Async operation completed');
          }, 1000);
        });
      
        console.log(result);
      
        console.log('End');
      }
      
      exampleAsyncFunction();
    • await 关键字只能在 async 函数内部使用。它用于等待一个 Promise 对象的解决。当遇到 await 时,async 函数会暂停执行,直到 Promise 对象解决。在上述例子中,await 会等待 setTimeout 的延时操作完成。

      • 需要注意的是,使用 await 时需要确保其所在的函数是 async 函数,否则会导致语法错误。
      js
      async function exampleAsyncFunction() {
        console.log('Start');
      
        const result = await someAsyncOperation(); // 必须在 async 函数内使用 await
      
        console.log(result);
      
        console.log('End');
      }
    • 使用 asyncawait 可以更清晰地处理 Promise 的链式调用,避免了回调地狱(Callback Hell)。

      js
      async function fetchData() {
        try {
          const response = await fetch('https://api.example.com/data');
          const data = await response.json();
          console.log(data);
        } catch (error) {
          console.error('Error fetching data:', error);
        }
      }
      
      fetchData();
      // fetchData 函数使用 await 依次等待 fetch 和 response.json() 的结果,使得异步操作看起来像同步代码一样,更易读
  2. 理解 EventLoop 和宏任务微任务执行顺序

    • 事件循环(Event Loop)是一个用于处理异步操作的机制。

    • 在事件循环中,任务被分为两类:宏任务(Macro Task)和微任务(Micro Task)。它们分别被放入不同的执行队列,并按照一定的规则执行。

    • Event Loop 执行过程

      • 执行同步任务(Synchronous Tasks): 从上到下依次执行脚本中的同步代码。
      • 执行微任务(Micro Tasks): 当执行栈空闲时,依次执行微任务队列中的所有任务。
      • 执行宏任务(Macro Tasks): 清空微任务队列后,从宏任务队列中取出一个任务执行。
  3. 了解 Promise.all 的作用和使用场景

    • Promise.all 是一个用于处理多个 Promise 并行执行的工具方法。它接收一个包含多个 Promise 的可迭代对象(通常是数组),并返回一个新的 Promise。

    • 这个新 Promise 会在所有输入的 Promise 都成功解决(resolved)时被解决,或者在任意一个输入的 Promise 被拒绝(rejected)时被拒绝。

    • Promise.all 返回的 Promise 的解决值是一个包含所有输入 Promise 解决值的数组。

    • 使用场景:

      • 并行执行多个异步任务: 当有多个异步任务可以并行执行,并且你希望等待所有任务完成后再执行下一步操作时,Promise.all 是一个很有用的工具。
      • 等待多个接口请求完成: 在前端开发中,有时需要从多个接口获取数据,然后进行下一步操作,使用 Promise.all 可以在所有数据都准备好时触发后续逻辑。
  4. 完成案例 - 学习反馈

    html
    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <!-- 初始化样式 -->
      <link rel="stylesheet" href="./css/reset.min.css">
      <!-- 引入 bootstrap.css -->
      <link href="./css/bootstrap.min.css" rel="stylesheet">
      <!-- 核心样式 -->
      <link rel="stylesheet" href="./css/index-min.css">
      <title>40 学习反馈</title>
    </head>
    
    <body>
      <div class="container">
        <div class="toast position-fixed top-0 start-50 translate-middle-x" role="alert" aria-live="assertive"
          aria-atomic="true" data-bs-delay="2000">
          <div class="toast-body">
            操作成功
          </div>
        </div>
        <h4 class="stu-title">学习反馈</h4>
        <img class="bg" src="./img/head.webp" alt="">
        <div class="item-wrap">
          <div class="hot-area">
            <span class="hot">热门校区</span>
            <ul class="nav">
              <li><a target="_blank" href="http://bjcp.itheima.com/">北京</a> </li>
              <li><a target="_blank" href="http://sh.itheima.com/">上海</a> </li>
              <li><a target="_blank" href="http://gz.itheima.com/">广州</a> </li>
              <li><a target="_blank" href="http://sz.itheima.com/">深圳</a> </li>
            </ul>
          </div>
          <form class="info-form">
            <div class="area-box">
              <span class="title">地区选择</span>
              <select name="province" class="province">
                <option value="">省份</option>
              </select>
              <select name="city" class="city">
                <option value="">城市</option>
              </select>
              <select name="area" class="area">
                <option value="">地区</option>
              </select>
            </div>
            <div class="area-box">
              <span class="title">您的称呼</span>
              <input type="text" name="nickname" class="nickname" value="播仔">
            </div>
            <div class="area-box">
              <span class="title">宝贵建议</span>
              <textarea type="text" name="feedback" class="feedback" placeholder="您对AJAX阶段课程宝贵的建议"></textarea>
            </div>
            <div class="area-box">
              <button type="button" class="btn btn-secondary submit">
                确定提交
              </button>
            </div>
          </form>
        </div>
      </div>
      <script src="./js/axios.min.js"></script>
      <script src="./js/form-serialize.js"></script>
      <script src="./js/bootstrap.min.js"></script>
      <!-- 核心代码 -->
      <script src="./js/render.js"></script>
      <script src="./js/submit.js"></script>
    </body>
    
    </html>
    js
    /**
     * 目标 1:完成省市区下拉列表切换
     *  1.1 设置省份下拉菜单数据
     *  1.2 切换省份,设置城市下拉菜单数据,清空地区下拉菜单
     *  1.3 切换城市,设置地区下拉菜单数据
     */
    
    // 1 设置省份下拉菜单数据
    const provinceElement = document.querySelector('.province');
    const cityElement = document.querySelector('.city');
    const aeraElement = document.querySelector('.area');
    
    axios
      .get('http://hmajax.itheima.net/api/province')
      .then((res) => {
        console.log(res);
        provinceElement.innerHTML = `<option value="">请选择省份</option>${res.data.list
          .map((item) => `<option value="${item}">${item}</option>`)
          .join('')}`;
      })
      .catch((error) => {
        console.dir(error);
      });
    
    // 2 切换省份,设置城市下拉菜单数据,清空地区下拉菜单
    provinceElement.addEventListener('change', async function () {
      const province = this.value;
      try {
        const res = await axios.get(`http://hmajax.itheima.net/api/city?pname=${province}`);
        console.log(res);
        cityElement.innerHTML = `<option value="">请选择城市</option>${res.data.list
          .map((item) => `<option value="${item}">${item}</option>`)
          .join('')}`;
        aeraElement.innerHTML = '<option value="">请选择区县</option>';
      } catch (error) {
        console.dir(error);
      }
    });
    
    // 3 切换城市,设置地区下拉菜单数据
    cityElement.addEventListener('change', async function () {
      const city = this.value;
      try {
        const res = await axios.get(`http://hmajax.itheima.net/api/area?pname=${provinceElement.value}&cname=${city}`);
        console.log(res);
        aeraElement.innerHTML = `<option value="">请选择区县</option>${res.data.list
          .map((item) => `<option value="${item}">${item}</option>`)
          .join('')}`;
      } catch (error) {
        console.dir(error);
      }
    });
    js
    /**
     * 目标 2:收集数据提交保存
     *  2.1 监听提交的点击事件
     *  2.2 依靠插件收集表单数据
     *  2.3 基于 axios 提交保存,显示结果
     */
    
    // 1 监听提交的点击事件
    const submitBtn = document.querySelector('.submit');
    submitBtn.addEventListener('click', async () => {
      // 2 依靠插件收集表单数据
      const form = document.querySelector('.info-form');
      const formData = serialize(form, { hash: true, empty: true });
    
      const toastDom = document.querySelector('.toast');
      const toast = new bootstrap.Toast(toastDom);
    
      // toastDom.querySelector('.toast-body').style.backgroundColor = 'var(--bs-primary)';
      toastDom.querySelector('.toast-body').classList.add('p-3', 'rounded-3');
    
      // 3 基于 axios 提交保存,显示结果
      try {
        const res = await axios.post('http://hmajax.itheima.net/api/feedback', formData);
        console.log(res);
        // HTTP 状态码:200
        if (res.status === 200) {
          // alert(res.data.message);
          toastDom.querySelector('.toast-body').textContent = res.data.message;
          toastDom.querySelector('.toast-body').style.backgroundColor = 'var(--bs-success)';
          toast.show();
        }
      } catch (err) {
        console.dir(err);
        toastDom.querySelector('.toast-body').textContent = err.response.data.message;
        toastDom.querySelector('.toast-body').style.backgroundColor = 'var(--bs-danger)';
        toast.show();
      }
    });

今日作业

客观题

在线答题:Day04_AJAX 进阶

  1. 以下哪个代码能正确运行?

    js
    const p1 = new Promise((resolve, reject) => {
      resolve(1);
    });
    
    const res1 = await p1;
    js
    const p2 = new Promise((resolve, reject) => {
      reject(2);
    });
    
    const res2 = await p2;
    js
    const p3 = new Promise((resolve, reject) => {
      resolve(3)
    })
    
    async const myFn = () => {
      const res3 = await p3
    }
    myFn()
    js
    const p4 = new Promise((resolve, reject) => {
      resolve(4);
    });
    
    const myFn4 = async () => {
      const res4 = await p4;
    };
    myFn4();
    答案
    • 答案是 B。在 JavaScript 中,await 关键字只能在 async 函数内部使用。选项 B 中的代码使用了正确的语法结构,即在 async 函数内使用 await
    • 选项 A 没有将 await 放在 async 函数内部。
    • 选项 C 中的 async const myFn = () => {...} 缺少括号。
    • 选项 D 中的 const p4 = new Promise(...} 缺少括号,且 async const myFn4 = () => {...} 缺少括号。
  2. 以下代码输出结果是?

    js
    const fn = async () => {
      const res = await 10;
      console.log(res);
    };
    fn();
    • A. 报错
    • B. 10
    • C. await 后面不是 Promise, 所以执行失败
    • D. undefined
    答案
    • 答案是 B. 10。
    • 在这个代码片段中,await 后面紧跟的是一个非 Promise 对象(数字 10),但是在 async 函数内部,非 Promise 对象会被自动包装成一个 resolved 的 Promise 对象。因此,整个 async 函数会正常执行,并输出结果。
    • 所以,输出结果是 10。
  3. 以下代码输出结果是?

    js
    let p = new Promise((resolve, reject) => {
      resolve(1000);
    });
    
    let p2 = new Promise((resolve, reject) => {
      reject(1000);
    });
    
    const theFn = async () => {
      const res = await p;
      console.log(res);
      const res2 = await p2;
      console.log(res2);
    };
    theFn();
    • A. 1000, 1000
    • B. 报错,一个都不打印
    • C. 不报错,但也不打印
    • D. 1000, 报错
    答案
    • 答案是 D. 1000, 报错
    • 在这个代码片段中,第一个 await p 会正常执行并输出 1000,但第二个 await p2 由于 p2 是一个 rejected 的 Promise,会导致 Promise 的状态变为 rejected,触发 await 后面的代码块的异常处理。
    • 因此,输出结果是 1000,然后会抛出一个异常,但由于 theFn 函数是一个异步函数,所以不会影响整个程序的执行。
  4. 以下代码能正确捕获到异常的是?

    js
    let p = new Promise((resolve, reject) => {
      reject(new Error('请检查'));
    });
    
    // A:
    // async const fnA = () => {
    //   try {
    //       const res = await p
    //   } catch (err) {
    //       console.error(err)
    //   }
    // }
    
    // B:
    // const fnB = async () => {
    //   try {
    //   } catch (err) {
    //     const res = await p;
    //     console.error(err);
    //   }
    // };
    
    // C:
    // const fnC = async () => {
    //   try {
    //     const res = await p;
    //   } catch {
    //     console.error(err);
    //   }
    // };
    
    // D:
    // const fnD = async () => {
    //   try {
    //     const res = await p;
    //   } catch (err) {
    //     console.error(err);
    //   }
    // };
    答案
    • 答案是 D.在这个代码片段中,await p 处会触发 reject 状态,进入到 catch 块中,正确捕获到异常并输出错误信息。
    • 选项 A 中的 async const 是不正确的语法。
    • 选项 B 中的 try 块为空,没有实际的异步操作。
    • 选项 C 中的 catch 块没有声明 err 参数。
  5. 以下代码都有哪些任务?

    js
    console.log(1);
    
    setTimeout(() => {
      console.log(2);
    }, 0);
    
    let p = new Promise((resolve, reject) => {
      resolve(3);
    });
    
    p.then((res) => {
      console.log(res);
    });
    
    console.log(4);
    • A. 只有同步任务
    • B. 只有异步任务
    • C. 同步任务,微任务,宏任务
    • D. 只有宏任务和微任务
    答案
    • 答案是 C. 同步任务、微任务(Promise 的回调函数)、宏任务(setTimeout 回调函数)。

      • console.log(1); 是一个同步任务,会在主线程上立即执行。
      • setTimeout(() => { console.log(2); }, 0); 是一个宏任务,会被放入任务队列中,等待主线程任务执行完毕后执行。
      • let p = new Promise((resolve, reject) => { resolve(3); }); 是一个同步任务,Promise 的执行是同步的,但它的 then 方法中的回调是微任务,会在当前任务执行完成后立即执行。
      • console.log(4); 是一个同步任务,会在主线程上立即执行。
    • 所以,该代码包含同步任务、微任务(Promise 的回调函数)、宏任务(setTimeout 回调函数)。

  6. 以下代码打印结果是?

    js
    // 读题回答打印顺序
    console.log(1);
    
    myFn();
    
    setTimeout(() => {
      theFn();
    }, 0);
    
    new Promise((resolve, reject) => {
      resolve(2);
    }).then((res) => {
      console.log(res);
    });
    
    function myFn() {
      console.log(3);
    }
    
    const theFn = () => {
      console.log(4);
    };
    • A. 1, 2, 4, 3
    • B. 1, 3, 4, 2
    • C. 1, 2, 3, 4
    • D. 1, 3, 2, 4
    答案
    • 答案是 B. 1, 3, 4, 2。

      • console.log(1); 是一个同步任务,会在主线程上立即执行。
      • myFn(); 是一个同步任务,会在主线程上立即执行,打印 3。
      • setTimeout(() => { theFn(); }, 0); 是一个宏任务,会被放入任务队列中,等待主线程任务执行完毕后执行。
      • new Promise((resolve, reject) => { resolve(2); }).then((res) => { console.log(res); }); 是一个同步任务,Promise 的执行是同步的,但它的 then 方法中的回调是微任务,会在当前任务执行完成后立即执行,打印 2。
      • theFn 是一个函数声明,会被提升到作用域顶部,所以在 setTimeout 中能够访问到,theFn(); 执行时打印 4。
    • 因此,输出结果是:1, 3, 4, 2。

  7. 以下代码打印结果是?

    js
    // 读题回答打印顺序
    async function async1() {
      console.log(1);
      await async2();
      console.log(2);
    }
    async function async2() {
      return new Promise((resolve, reject) => {
        reject(new Error(''));
      });
    }
    console.log(3);
    
    setTimeout(function () {
      console.log(4);
    }, 0);
    
    async1();
    
    new Promise(function (resolve) {
      console.log(5);
      resolve();
    }).then(function () {
      console.log(6);
    });
    
    console.log(7);
    • A. 3, 1, 7, 5, 6, 4
    • B. 3, 1, 5, 7, 6, 4
    • C. 3, 5, 1, 7, 6, 4
    • D. 3, 6, 1, 7, 5, 4
    答案
    • 答案是 A. 3, 1, 7, 5, 6, 4。

      • console.log(3); 是一个同步任务,会在主线程上立即执行。
      • setTimeout(function () { console.log(4); }, 0); 是一个宏任务,会被放入任务队列中,等待主线程任务执行完毕后执行。
      • async1(); 被调用,async1 是一个异步函数,其中的 console.log(1); 是同步执行的,然后遇到 await async2();async2 返回的 Promise 是 rejected 状态,触发 async1 中的异常,但由于异步函数的特性,异常会被 Promise 包装,不会中断主线程,所以会继续执行 console.log(2);
      • new Promise(function (resolve) { console.log(5); resolve(); }).then(function () { console.log(6); }); 是一个同步任务,Promise 的执行是同步的,但它的 then 方法中的回调是微任务,会在当前任务执行完成后立即执行,打印 5 和 6。
      • 最后执行 setTimeout 中的回调,打印 4。
      • console.log(7); 是一个同步任务,会在主线程上立即执行。
    • 所以,输出结果是:3, 1, 7, 5, 6, 4。

  8. 以下代码打印结果是?

    js
    // 读题回答打印顺序
    setTimeout(() => {
      console.log(1);
    
      new Promise((resolve, reject) => {
        resolve(2);
      }).then((res) => {
        console.log(res);
    
        setTimeout(() => {
          console.log(3);
        }, 1000);
      });
    }, 0);
    
    console.log(4);
    
    setTimeout(() => {
      console.log(5);
    }, 5000);
    
    console.log(6);
    • A. 4, 1, 6, 2, 3, 5
    • B. 4, 6, 1, 2, 5, 3
    • C. 4, 6, 1, 2, 3, 5
    • D. 4, 6, 2, 1, 3, 5
    答案
    • 答案是 C. 4, 6, 1, 2, 3, 5。

      • 首先执行第一个 setTimeout,打印 1。然后执行内部的 Promise,由于是 resolved 状态,执行其 then 回调,打印 2,并设置另一个 setTimeout,打印 3。
      • 继续执行主线程,打印 4。
      • 执行第二个 setTimeout,由于设置了延时为 5000 毫秒,所以会在 5000 毫秒后执行,打印 5。
      • 最后打印 6。
    • 所以,输出结果是:4, 6, 1, 2, 3, 5。

  9. 以下代码打印结果是?

    js
    // 读题回答打印顺序
    new Promise((resolve, reject) => {
      console.log(1);
    
      new Promise((resolve, reject) => {
        console.log(2);
    
        setTimeout(() => {
          console.log(3);
        }, 0);
    
        console.log(4);
      });
    
      console.log(5);
    });
    
    setTimeout(() => {
      console.log(6);
    }, 1000);
    
    console.log(7);
    • A. 1, 2, 4, 5, 3, 7, 6
    • B. 1, 3, 4, 5, 7, 2, 6
    • C. 1, 2, 5, 4, 7, 3, 6
    • D. 1, 2, 4, 5, 7, 3, 6
    答案
    • 答案是 C. 1, 2, 5, 4, 7, 3, 6。

      • 首先执行外部的 Promise,打印 1。然后执行内部的 Promise,打印 2。
      • 在内部的 Promise 中,遇到 setTimeout,将其放入宏任务队列中,接着打印 4。
      • 继续执行外部的 Promise,打印 5。
      • 主线程任务执行完成后,开始执行宏任务队列中的 setTimeout,打印 3。
      • 执行最后的 setTimeout,由于设置了延时为 1000 毫秒,所以会在 1000 毫秒后执行,打印 6。
      • 最后打印 7。
    • 所以,输出结果是:1, 2, 5, 4, 7, 3, 6。

  10. 以下代码打印结果是?

    js
    console.log(1);
    
    setTimeout(() => {
      console.log(2);
    }, 0);
    
    console.log(3);
    
    console.log(4);
    
    setTimeout(() => {
      console.log(5);
    }, 0);
    
    console.log(6);
    • A. 1, 3, 6, 4, 2, 5
    • B. 1, 3, 4, 6, 2, 5
    • C. 1, 4, 3, 6, 2, 5
    • D. 1, 3, 4, 2, 6, 5
    答案
    • 答案是 D. 1, 3, 4, 2, 6, 5。

      • 执行第一个 console.log(1);,打印 1。
      • 执行第一个 setTimeout,将其放入宏任务队列中,继续执行同步任务,打印 3。
      • 打印 4。
      • 主线程同步任务执行完成后,开始执行宏任务队列中的 setTimeout,打印 2。
      • 继续执行同步任务,打印 6。
      • 执行第二个 setTimeout,将其放入宏任务队列中,但由于前面的任务已经执行完成,所以可以立即执行,打印 5。
    • 所以,输出结果是:1, 3, 4, 2, 6, 5。

主观题

作业 1 - 事件循环面试题

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>事件循环经典 经典面试题 1</title>
</head>

<body>
  <script>
    console.log(1) // 进入调用栈,执行同步代码
    setTimeout(() => { // 宿主环境(浏览器),0s 后将回调函数放入任务队列
      console.log(2)
    }, 0)
    console.log(3) // 进入调用栈,执行同步代码
  </script>
  <script>
    console.log(4) // 进入调用栈,执行同步代码
    setTimeout(() => { // 宿主环境(浏览器),0s 后将回调函数放入任务队列
      console.log(5)
    }, 0)
    console.log(6) // 进入调用栈,执行同步代码

    // 调用栈:
    // 1. console.log(1);
    // 2. console.log(3);
    // 3. console.log(4);
    // 4. console.log(6);

    // 宿主环境(浏览器)
    // 1. setTimeout(() => { console.log(2); }, 0);
    // 2. setTimeout(() => { console.log(5); }, 0);

    // 任务队列 - 宏任务:
    // 1. console.log(2);
    // 2. console.log(5);

    // 打印结果:
    // 1 3 4 6 2 5
  </script>
</body>

</html>
html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>事件循环经典 经典面试题 2</title>
</head>

<body>
  <!-- 今日头条 -->
  <!-- 提示:await 右结合,右边代码执行后原地等待(await 取代 then 函数,相当于微任务) -->
  <script>
    console.log(1) // 进入调用栈,执行同步代码
    async function fnOne() {
      console.log(2)
      await fnTwo() // 右结合先执行右侧的代码,然后等待
      console.log(3)
    }
    async function fnTwo() {
      console.log(4)
    }
    fnOne()
    setTimeout(() => { // 宿主环境(浏览器),2s 后将回调函数放入任务队列
      console.log(5)
    }, 2000)
    let p = new Promise((resolve, reject) => { // new Promise() 里的函数体会马上执行所有代码
      console.log(6)
      resolve()
      console.log(7)
    })
    setTimeout(() => { // 宿主环境(浏览器),0s 后将回调函数放入任务队列
      console.log(8)
    }, 0)
    p.then(() => { // Promise.then 回调函数,执行异步代码
      console.log(9)
    })
    console.log(10) // 进入调用栈,执行同步代码

    // 调用栈
    // 1. console.log(1)
    // 2. fnOne() { console.log(2) }
    // 3. fnTwo() { console.log(4) }
    // 4. let p = new Promise((resolve, reject) => { console.log(6);  resolve(); console.log(7); }
    // 5. console.log(10)

    // 任务队列 - 宏任务:
    // 1. setTimeout(() => { console.log(5) }, 2000)
    // 2. setTimeout(() => { console.log(8) }, 0)

    // 任务队列 - 微任务:
    // 1. console.log(3)
    // 2. p.then(() => { console.log(9) })

    // 打印结果:
    // 1 2 4 6 7 10 3 9 8 5
  </script>
</body>

</html>
html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>事件循环经典 经典面试题 3</title>
</head>

<body style="width: 100%; height: 100vh;">
  <!-- 需要给 body 设置宽高,如果没有设置,则无法点击,绑定的点击事件会无效 -->
  <script>
    // Twitter 面试题 - 20% 答对
    document.body.addEventListener('click', (e) => {
      let p = new Promise(resolve => resolve(1))
      p.then(result => console.log(result)) // 微任务:Promise 的回调函数(即 then 方法中的函数)是异步代码
      console.log(2) // 同步代码
    })
    document.body.addEventListener('click', () => {
      let p = new Promise(resolve => resolve(3))
      p.then(result => console.log(result))
      console.log(4)
    })

    // 调用栈:
    // 1. console.log(2);

    // 微任务:
    // 1. p.then(result => console.log(result))

    // 打印结果:
    // (用户点击后) 2 1 4 3
  </script>
</body>

</html>

作业 2 - 评论列表

  • 目标:完成如下评论列表效果

  • 要求:

    • 默认上来展示所有评论列表数据(注意不区分用户了)感受下大家数据互相影响,也可以看到别人评论
    • 新增评论功能
    • 删除评论功能
    • 分页切换评论列表数据功能
    • 删除最后一条评论,列表要自动回到上一页
  • 接口文档:https://apifox.com/apidoc/shared-1b0dd84f-faa8-435d-b355-5a8a329e34a8/api-82668108

  • 配套资料:配套标签和样式在文件夹内

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>评论列表</title>
  <link rel="stylesheet" href="./css/bootstrap.min.css">
  <style>
    .badge {
      float: right;
      margin-right: 5px;
    }

    .my-page {
      margin: 0 5px;
    }

    .all-page-content {
      margin-left: 5px;
    }
  </style>
</head>

<body style="padding: 15px;">

  <!-- 评论面板 -->
  <div class="panel panel-primary">
    <div class="panel-heading">
      <h3 class="panel-title">发表评论</h3>
    </div>
    <form class="panel-body cmt-form">
      <div>评论人:</div>
      <input type="text" class="form-control" name="username" autocomplete="off" />
      <div>评论内容:</div>
      <textarea class="form-control" name="content"></textarea>
      <button type="submit" class="btn btn-primary submit">发表评论</button>
    </form>
  </div>


  <!-- 评论列表 -->
  <ul class="list-group">
    <!-- <li class="list-group-item">
       <span>评论内容</span>
       <span class="badge del" style="cursor:pointer; background-color: lightgray;">删除</span>
       <span class="badge" style="background-color: #F0AD4E;">评论时间:xxx</span>
       <span class="badge" style="background-color: #5BC0DE;">评论人:xxx</span>
    </li> -->
  </ul>

  <!-- 分页器 -->
  <nav>
    <ul class="pagination">
      <li>
        <button class="last">
          <span>&laquo;</span>
        </button>
      </li>
      <li class="my-page">
        <span class="page-show"></span>
      </li>
      <li>
        <button class="next">
          <span>&raquo;</span>
        </button>
      </li>
      <li class="all-page-content">
        <span>共计:<span class="all-page"></span>页</span>
      </li>
    </ul>
  </nav>
  <script src="./js/axios.min.js"></script>
  <script src="./js/form-serialize.js"></script>
  <script src="./js/getComment.js"></script>
  <script src="./js/addComment.js"></script>
  <script src="./js/delComment.js"></script>
</body>

</html>
js
// 1 默认上来展示所有评论列表数据(注意不区分用户了)感受下大家数据互相影响,也可以看到别人评论
// api: https://hmajax.itheima.net/api/cmtlist?page=${nowPage} GET
let nowPage = 1;
let allPage;

const listGroup = document.querySelector('.list-group');

function fetchCommentList() {
  axios.get(`https://hmajax.itheima.net/api/cmtlist?page=${nowPage}`).then((res) => {
    console.log(res);
    // 总页码数
    allPage = res.data.allPage;
    document.querySelector('.all-page').innerText = allPage;

    // 清空列表
    listGroup.innerHTML = '';

    // 渲染列表
    listGroup.innerHTML = res.data.data
      .map((item) => {
        return `<li class="list-group-item">
        <span>${item.content}</span>
        <span class="badge del" style="cursor:pointer; background-color: lightgray;" data-id=${item.id}>删除</span>
        <span class="badge" style="background-color: #F0AD4E;">评论时间:${item.time}</span>
        <span class="badge" style="background-color: #5BC0DE;">评论人:${item.username}</span>
      </li>`;
      })
      .join('');

    // 设置页码
    document.querySelector('.page-show').innerHTML = nowPage;
  });
}
// 页面加载时默认拉取第一页的评论数据
fetchCommentList();

// 绑定 上一页/下一页按钮的点击事件
const lastElement = document.querySelector('.last');
const nextElement = document.querySelector('.next');

// 分页切换评论列表数据功能
if (lastElement && nextElement) {
  lastElement.addEventListener('click', () => {
    nowPage > 1 ? nowPage-- && fetchCommentList() : console.log('已经是第一页了');
  });
  nextElement.addEventListener('click', () => {
    nowPage < allPage ? nowPage++ && fetchCommentList() : console.log('已经是最后一页了');
  });
}
js
// 新增评论功能
// api: https://hmajax.itheima.net/api/addcmt POST
// data:username content

const submitElement = document.querySelector('.submit');
submitElement.addEventListener('click', async (e) => {
  e.preventDefault(); // 阻止默认行为

  // 获取表单数据
  const form = document.querySelector('.cmt-form');
  const formData = serialize(form, { hash: true, empty: true });

  try {
    const response = await axios.post('https://hmajax.itheima.net/api/addcmt', formData);
    console.log(response);

    // 重新拉取评论列表数据
    nowPage = 1;
    fetchCommentList();

    // 表单复位
    form.reset();
  } catch (error) {
    console.error(error);
  }
});
js
// 删除评论功能
// 删除最后一条评论,列表要自动回到上一页
// api: https://hmajax.itheima.net/api/delcmt?id=${id} GET

listGroup.addEventListener('click', async (e) => {
  console.log(e.target);

  if (e.target.classList.contains('del')) {
    try {
      const response = await axios.get(`https://hmajax.itheima.net/api/delcmt?id=${e.target.getAttribute('data-id')}`);
      console.log(response);
      allPage = response.data.allPage;
      nowPage = nowPage > allPage ? allPage : nowPage;
      fetchCommentList();
    } catch (error) {
      console.dir(error);
    }
  }
});

面试题

  1. 你是怎么理解事件代理的?

    参考文档

  2. 说说你对事件循环的理解?

    参考文档

参考文献