Ajax 笔记(四)—— Ajax 进阶

笔记目录

  • [4. Ajax 进阶](#4. Ajax 进阶)
    • [4.1 同步代码和异步代码](#4.1 同步代码和异步代码)
    • [4.2 回调函数地狱](#4.2 回调函数地狱)
      • [4.2.1 解决方法一:Promise 链式调用](#4.2.1 解决方法一:Promise 链式调用)
      • [4.2.2 解决方法二:async 函数和 await](#4.2.2 解决方法二:async 函数和 await)
    • [4.3 Promise.all 静态方法](#4.3 Promise.all 静态方法)
    • [4.4 事件循环](#4.4 事件循环)
      • [4.4.1 事件循环](#4.4.1 事件循环)
      • [4.4.2 宏任务与微任务](#4.4.2 宏任务与微任务)
    • [4.5 案例](#4.5 案例)
      • [4.5.1 案例一-商品分类](#4.5.1 案例一-商品分类)
      • [4.5.2 案例二-学习反馈](#4.5.2 案例二-学习反馈)

Ajax 笔记:

Ajax 笔记(一)------ Ajax 入门

Ajax 笔记(二)------ Ajax 案例

Ajax 笔记(三)------ Ajax 原理

Ajax 笔记(四)------ Ajax 进阶


Ajax 笔记接口文档:https://apifox.com/apidoc/shared-fa9274ac-362e-4905-806b-6135df6aa90e/doc-842135


4. Ajax 进阶

4.1 同步代码和异步代码

JavaScript 是单线程的语言,意味着它只有一个主线程用来执行代码。这个特点决定了 JavaScript 在同一时间只能处理一个任务,防止了多线程带来的竞态条件和死锁等问题。随着 HTML5 到来也支持了多线程webWorker,但也是不允许操作 DOM 。

  1. 同步代码:逐行执行,需原地等待结果后,才继续向下执行

  2. 异步代码:调用后耗时,不阻塞代码继续执行(不必原地等待),在将来完成后触发回调函数传递结果,比如:setTimeout / setInterval,事件,Ajax

4.2 回调函数地狱

  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]
    })
  })
})

4.2.1 解决方法一:Promise 链式调用

  1. 依靠 then() 方法会返回一个新生成的 Promise 对象特性,继串联下一环任务,直到结束。then() 回调函数中的返回值,会影响新生成的 Promise 对象最终状态和结果。

代码示例:

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 链式调用解决回调地狱
js 复制代码
/**
 * 目标:把回调函数嵌套代码,改成Promise链式调用结构
 * 需求:获取默认第一个省,第一个市,第一个地区并展示在下拉菜单中
*/
let pname = ''
// 1. 得到-获取省份Promise对象
axios({url: 'http://hmajax.itheima.net/api/province'}).then(result => {
  pname = result.data.list[0]
  document.querySelector('.province').innerHTML = pname
  // 2. 得到-获取城市Promise对象
  return axios({url: 'http://hmajax.itheima.net/api/city', params: { pname }})
}).then(result => {
  const cname = result.data.list[0]
  document.querySelector('.city').innerHTML = cname
  // 3. 得到-获取地区Promise对象
  return axios({url: 'http://hmajax.itheima.net/api/area', params: { pname, cname }})
}).then(result => {
  console.log(result)
  const areaName = result.data.list[0]
  document.querySelector('.area').innerHTML = areaName
})

4.2.2 解决方法二:async 函数和 await

在 async 函数内,使用 await 关键字取代 then 函数,等待获取 Promise 对象成功状态的结果值

  1. 利用async 函数和 await 解决回调地狱
js 复制代码
/**
 * 目标:掌握async和await语法,解决回调函数地狱
 * 概念:在async函数内,使用await关键字,获取Promise对象"成功状态"结果值
 * 注意:await必须用在async修饰的函数内(await会阻止"异步函数内"代码继续执行,原地等待结果)
*/
// 1. 定义async修饰函数
async function getData() {
  // 2. await等待Promise对象成功的结果
  const pObj = await axios({url: 'http://hmajax.itheima.net/api/province'})
  const pname = pObj.data.list[0]
  const cObj = await axios({url: 'http://hmajax.itheima.net/api/city', params: { pname }})
  const cname = cObj.data.list[0]
  const aObj = await axios({url: 'http://hmajax.itheima.net/api/area', params: { pname, cname }})
  const areaName = aObj.data.list[0]


  document.querySelector('.province').innerHTML = pname
  document.querySelector('.city').innerHTML = cname
  document.querySelector('.area').innerHTML = areaName
}

getData()
  1. async 函数和 await 捕获错误
js 复制代码
try {
  // 要执行的代码
} catch (error) {
  // error 接收的是,错误消息
  // try 里代码,如果有错误,直接进入这里执行
}

改进上述代码:

js 复制代码
/**
 * 目标:async和await_错误捕获
*/
async function getData() {
  // 1. try包裹可能产生错误的代码
  try {
    const pObj = await axios({ url: 'http://hmajax.itheima.net/api/province' })
    const pname = pObj.data.list[0]
    const cObj = await axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } })
    const cname = cObj.data.list[0]
    const aObj = await axios({ url: 'http://hmajax.itheima.net/api/area', params: { pname, cname } })
    const areaName = aObj.data.list[0]

    document.querySelector('.province').innerHTML = pname
    document.querySelector('.city').innerHTML = cname
    document.querySelector('.area').innerHTML = areaName
  } catch (error) {
    // 2. 接着调用catch块,接收错误信息
    // 如果try里某行代码报错后,try中剩余的代码不会执行了
    console.dir(error)
  }
}

getData()

4.3 Promise.all 静态方法

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

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

实例代码:

js 复制代码
<!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>Promise的all方法</title>
</head>

<body>
  <ul class="my-ul"></ul>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/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' } })

    // 2. 使用Promise.all,合并多个Promise对象
    const p = Promise.all([bjPromise, shPromise, gzPromise, szPromise])
    p.then(result => {
      // 注意:结果数组顺序和合并时顺序是一致
      console.log(result)
      const htmlStr = result.map(item => {
        return `<li>${item.data.data.area} --- ${item.data.data.weather}</li>`
      }).join('')
      document.querySelector('.my-ul').innerHTML = htmlStr
    }).catch(error => {
      console.dir(error)
    })
  </script>
</body>

</html>

4.4 事件循环

4.4.1 事件循环

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

举例说明:

js 复制代码
/**
 * 目标:阅读并回答执行的顺序结果
*/
console.log(1)
setTimeout(() => {
  console.log(2)
}, 0)
function myFn() {
  console.log(3)
}
function ajaxFn() {
  const xhr = new XMLHttpRequest()
  xhr.open('GET', 'http://hmajax.itheima.net/api/province')
  xhr.addEventListener('loadend', () => {
    console.log(4)
  })
  xhr.send()
}
for (let i = 0; i < 1; i++) {
  console.log(5)
}
ajaxFn()
document.addEventListener('click', () => {
  console.log(6)
})
myFn()

// 结果:1 5 3 2 4 点击一次document就会执行一次打印6

代码分析:

  1. console.log(1)进入调用栈,然后在控制台打印1
  2. setTimeout是异步代码,交给浏览器,console.log(2) 在 0 s 过后进入任务队列
  3. 然后两个 function 不调用,不执行
  4. for循环放入调用栈执行,打印5
  5. 执行ajaxFnloadend事件异步,交给浏览器
  6. 用户click异步点击事件,交给浏览器,和loadend一样处于监测状态
  7. myFn放入调用栈执行,打印3
  8. 同步代码执行完毕,调用栈清空,然后反复查找任务队列有无需要执行的回调函数
  9. 执行setTimeout,进入调用栈,打印2,出栈
  10. 浏览器中的loadend事件被放入任务队列,然后放入调用栈,执行,打印4
  11. 最后click事件,点击后打印6

4.4.2 宏任务与微任务

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

宏任务(Macrotasks)

  1. setTimeoutsetInterval:用于创建定时任务,会在指定的时间间隔之后执行回调函数。
  2. DOM 事件:如点击事件、输入事件等。
  3. 网络请求:如 Ajax 请求、fetch 等。
  4. I/O 操作:如文件读写、读取数据库等。
  5. requestAnimationFrame:用于在每次浏览器重绘之前执行的回调。
  6. MessageChannel:一种更底层的异步任务创建方式。
  7. UI 渲染:浏览器会在宏任务执行完成后进行 UI 渲染。

微任务(Microtasks)

  1. Promise 的 resolve 和 reject:当一个 Promise 被 resolved 或 rejected 时,相关的回调会被放入微任务队列。
  2. process.nextTick(Node.js 环境下):类似于 Promise 的微任务,但在 Node.js 环境中使用。
  3. Object.observeMutationObserver:用于监听对象的变化,在变化后会触发回调。
  4. Vue 的 nextTick:Vue 中用于延迟执行 DOM 更新之后的回调。
  5. await 表达式的后续操作:在 async 函数中,await 表达式后的操作会被放入微任务队列。

JavaScript 执行顺序如下:

  1. 执行同步代码:从上到下逐行执行当前代码文件中的同步代码,将函数调用压入执行栈,遇到异步任务会将其注册,但不会立即执行。

  2. 执行微任务:在每次事件循环中,首先会执行微任务队列中的所有微任务,直到微任务队列为空。这保证了微任务比宏任务更早执行。

  3. 执行宏任务:然后从宏任务队列中取出一个宏任务(通常是先进先出),执行它,可能触发异步操作。

  4. 可能进行 UI 渲染:在宏任务执行完成后,浏览器有机会进行 UI 渲染(如果有需要的话)。

  5. 重复以上步骤:然后事件循环继续,重复执行微任务、宏任务、可能的 UI 渲染等步骤。

实例代码:

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)

// 1 3 5 4 2

代码分析:

  1. console.log(1)进入调用栈,打印1
  2. setTiemout宏任务,交给浏览器,0sconsole.log(2)放入宏任务队列排队
  3. new Promise放入调用栈,执行console.log(3),打印3,用4标记成功,然后出栈
  4. p调用过resolve,处于成功状态,p.then()里面的回调函数是微任务,放入微任务队列排队
  5. console.log(5),进入调用栈打印5,出栈
  6. 调用栈清空,先调度微任务队列的回调函数
  7. 执行微任务console.log(result),打印4
  8. 接着调度宏任务console.log(2),打印2

4.5 案例

4.5.1 案例一-商品分类

js 复制代码
/**
 * 目标:把所有商品分类"同时"渲染到页面上
 *  1. 获取所有一级分类数据
 *  2. 遍历id,创建获取二级分类请求
 *  3. 合并所有二级分类Promise对象
 *  4. 等待同时成功后,渲染页面
*/
// 1. 获取所有一级分类数据
axios({
  url: 'http://hmajax.itheima.net/api/category/top'
}).then(result => {
  console.log(result)
  // 2. 遍历id,创建获取二级分类请求
  const secPromiseList = result.data.data.map(item => {
    return axios({
      url: 'http://hmajax.itheima.net/api/category/sub',
      params: {
        id: item.id // 一级分类id
      }
    })
  })
  console.log(secPromiseList) // [二级分类请求Promise对象,二级分类请求Promise对象,...]
  // 3. 合并所有二级分类Promise对象
  const p = Promise.all(secPromiseList)
  p.then(result => {
    console.log(result)
    // 4. 等待同时成功后,渲染页面
    const htmlStr = result.map(item => {
      const dataObj = item.data.data // 取出关键数据对象
      return `<div class="item">
    <h3>${dataObj.name}</h3>
    <ul>
      ${dataObj.children.map(item => {
        return `<li>
        <a href="javascript:;">
          <img src="${item.picture}">
          <p>${item.name}</p>
        </a>
      </li>`
      }).join('')}
    </ul>
  </div>`
    }).join('')
    console.log(htmlStr)
    document.querySelector('.sub-list').innerHTML = htmlStr
  })
})

4.5.2 案例二-学习反馈

js 复制代码
/**
 * 目标1:完成省市区下拉列表切换
 *  1.1 设置省份下拉菜单数据
 *  1.2 切换省份,设置城市下拉菜单数据,清空地区下拉菜单
 *  1.3 切换城市,设置地区下拉菜单数据
 */
// 1.1 设置省份下拉菜单数据
axios({
  url: 'http://hmajax.itheima.net/api/province'
}).then(result => {
  const optionStr = result.data.list.map(pname => `<option value="${pname}">${pname}</option>`).join('')
  document.querySelector('.province').innerHTML = `<option value="">省份</option>` + optionStr
})

// 1.2 切换省份,设置城市下拉菜单数据,清空地区下拉菜单
document.querySelector('.province').addEventListener('change', async e => {
  // 获取用户选择省份名字
  // console.log(e.target.value)
  const result = await axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname: e.target.value } })
  const optionStr = result.data.list.map(cname => `<option value="${cname}">${cname}</option>`).join('')
  // 把默认城市选项+下属城市数据插入select中
  document.querySelector('.city').innerHTML = `<option value="">城市</option>` + optionStr

  // 清空地区数据
  document.querySelector('.area').innerHTML = `<option value="">地区</option>`
})

// 1.3 切换城市,设置地区下拉菜单数据
document.querySelector('.city').addEventListener('change', async e => {
  console.log(e.target.value)
  const result = await axios({url: 'http://hmajax.itheima.net/api/area', params: {
    pname: document.querySelector('.province').value,
    cname: e.target.value
  }})
  console.log(result)
  const optionStr = result.data.list.map(aname => `<option value="${aname}">${aname}</option>`).join('')
  console.log(optionStr)
  document.querySelector('.area').innerHTML = `<option value="">地区</option>` + optionStr
})

/**
 * 目标2:收集数据提交保存
 *  2.1 监听提交的点击事件
 *  2.2 依靠插件收集表单数据
 *  2.3 基于axios提交保存,显示结果
 */
// 2.1 监听提交的点击事件
document.querySelector('.submit').addEventListener('click', async () => {
  // 2.2 依靠插件收集表单数据
  const form = document.querySelector('.info-form')
  const data = serialize(form, { hash: true, empty: true })
  console.log(data)
  // 2.3 基于axios提交保存,显示结果
  try {
    const result = await axios({
      url: 'http://hmajax.itheima.net/api/feedback',
      method: 'POST',
      data
    })
    console.log(result)
    alert(result.data.message)
  } catch (error) {
    console.dir(error)
    alert(error.response.data.message)
  }
})
相关推荐
世俗ˊ38 分钟前
CSS入门笔记
前端·css·笔记
万河归海4281 小时前
C语言——二分法搜索数组中特定元素并返回下标
c语言·开发语言·数据结构·经验分享·笔记·算法·visualstudio
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
李小星同志2 小时前
高级算法设计与分析 学习笔记6 B树
笔记·学习
霜晨月c3 小时前
MFC 使用细节
笔记·学习·mfc
Jhxbdks3 小时前
C语言中的一些小知识(二)
c语言·开发语言·笔记
AlexMercer10123 小时前
【C++】二、数据类型 (同C)
c语言·开发语言·数据结构·c++·笔记·算法
微刻时光4 小时前
Redis集群知识及实战
数据库·redis·笔记·学习·程序人生·缓存
chnyi6_ya4 小时前
一些写leetcode的笔记
笔记·leetcode·c#
青椒大仙KI115 小时前
24/9/19 算法笔记 kaggle BankChurn数据分类
笔记·算法·分类