Skip to content

04 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 属性值。
      • 在这个例子中,第二个 option 的 value 属性是 "南京"。
    • 页面上看到的是北京,还是北京市?

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

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

学习目标

  1. 区分异步代码,回调函数地狱问题和所有解决防范(Promise 链式调用)
  2. 掌握 async 和 await 使用
  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 (error) {
        // 2. 接着调用 catch 块,接收错误信息
        // 如果 try 里某行代码报错后,try 中剩余的代码不会执行了
        console.dir(error);
      }
    }

    getData();

  </script>
</body>

</html>

小结 ​

  1. await 的作用是什么?

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

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

事件循环 ​

  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');
      }
    • 使用 async 和 await 可以更清晰地处理 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. 说说你对事件循环的理解?

    参考文档

参考文献 ​