Ajax的原理与进阶

Ajax的原理与进阶

cccs7 Lv5

异步 JavaScript 和 XML,或 Ajax 本身不是一种技术,而是一种将一些现有技术结合起来使用的方法,包括:HTML XHTML CSS JavaScript DOM XML XSLT 、以及最重要的 XMLHttpRequest 对象。当使用结合了这些技术的 Ajax 模型以后,网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面。这使得程序能够更快地回应用户的操作。Ajax 最吸引人的特性是它的“异步”性质,这意味着它可以与服务器通信、交换数据并更新页面,而无需刷新页面。

尽管 Ajax 中的 X 代表 XML,但是 JSON 才是首选,因为它更加轻量,而且是用 JavaScript 编写的。在 Ajax 模型中,JSON 和 XML 都被用来包装信息。

  • 使用 浏览器的 XMLHttpRequest 对象 与 服务器 通信
  • 浏览器网页中,使用 ajax 技术(XHR对象) 发送获取数据的请求,服务器代码响应准备好的数据给 前端,前端拿到数组后 渲染到网页

快速入门

使用语法

1
2
3
4
5
axios({
url: '目标资源地址'
}).then((result) => {
// 对服务器返回的数据做后续处理
})
  • 请求的 url 地址就是 标记资源的网址

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>

<style>
.btn {
width: 100px;
height: 50px;
background: gray;
border-radius: 7px;
}
</style>
</head>
<body>
<!--
axios库地址:https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js
省份数据地址:http://hmajax.itheima.net/api/province

目标: 使用axios库, 获取省份列表数据, 展示到页面上
1. 引入axios库
-->
<button class="btn"></button>
<p class="my-p"></p>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
document.querySelector(".btn").addEventListener("click", function () {
//使用 axios 函数
axios({
url: "http://hmajax.itheima.net/api/province",
}).then((result) => {
console.log(result);
console.log(result.data.list);
console.log(result.data.list.join("<br>"));
document.querySelector(".my-p").innerHTML =
result.data.list.join("<br>");
});
});
</script>
</body>
</html>

URL 查询参数


  1. 什么是查询参数 ?

    • 携带给服务器额外信息,让服务器返回我想要的某一部分数据而不是全部数据

    • 举例:查询河北省下属的城市列表,需要先把河北省传递给服务器

      image-20230404101257205

  2. 查询参数的语法 ?

如何携带参数

axios 怎么携带查询参数? —-> 使用 params 选项即可

1
2
3
4
5
6
7
8
axios({
url: 'URL',
params: {
参数: 值
}
}).then( result => {
// 对响应的数据做后续处理
})
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body>
<button class="btn"></button>
<p class="my-p"></p>

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
document.querySelector(".btn").addEventListener("click", function () {
axios({
url: "http://hmajax.itheima.net/api/city",
params: {
pname: "河南省",
},
}).then((result) => {
console.log(result.data.list);
document.querySelector(".my-p").innerHTML = result.data.list.join('<br>');
});
});
</script>
</body>

常用请求方法和数据提交


常用的请求方法

好的,常用的 HTTP 请求方法有以下几种:

  1. GET:用于获取资源,通常用于查询数据。GET 请求的特点是幂等,也就是说多次请求同一个 URL,得到的结果是相同的,不会对服务器产生影响。

  2. POST:用于提交数据,通常用于创建资源。POST 请求的特点是非幂等,也就是说多次请求同一个 URL,可能会创建多个资源。

  3. PUT:用于更新资源,通常用于更新完整的数据。PUT 请求的特点是幂等,也就是说多次请求同一个 URL,得到的结果是相同的,会覆盖原有的数据。

  4. PATCH:用于更新资源,通常用于更新部分数据。PATCH 请求的特点是幂等,也就是说多次请求同一个 URL,得到的结果是相同的,只会更新指定的字段。

  5. DELETE:用于删除资源,通常用于删除数据。DELETE 请求的特点是幂等,也就是说多次请求同一个 URL,得到的结果是相同的,只会删除一次资源。

需要注意的是,HTTP 请求方法的使用应该符合语义化的规范,例如使用 GET 请求获取数据,使用 POST 请求提交数据等,避免滥用某些方法导致不必要的安全问题。

数据提交

1
2
3
4
5
6
7
8
9
axios({
url: '目标资源地址',
method: '请求方法',
data: {
参数名: 值
}
}).then(result => {
// 对服务器返回的数据做后续处理
})

axios 错误处理


axios 提供了多种方式来处理请求过程中的错误,例如网络错误、响应状态码错误、超时等。常用的错误处理方式有以下几种:

  1. 使用 catch 方法捕获错误:可以在 axios 请求链中使用 catch 方法来捕获请求过程中的错误。在 catch 方法中,可以对错误进行统一的处理,例如输出错误信息、显示提示框等。
1
2
3
4
5
6
7
axios.get('/api/data')
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error(error);
});
  1. 使用 error 属性捕获错误:可以在 Promise 回调函数中使用 error 属性来捕获请求过程中的错误。在 error 属性中,可以对错误进行统一的处理,例如输出错误信息、显示提示框等。
1
2
3
4
5
6
axios.get('/api/data')
.then(response => {
console.log(response.data);
}, error => {
console.error(error);
});
  1. 使用响应拦截器统一处理错误:可以在响应拦截器中统一处理响应过程中的错误,例如判断响应状态码是否为 401,如果是则跳转到登录页面。
1
2
3
4
5
6
7
8
9
axios.interceptors.response.use(response => {
return response;
}, error => {
if (error.response.status === 401) {
// 跳转到登录页面
window.location.href = '/login';
}
return Promise.reject(error);
});

需要注意的是,错误处理应该根据具体的业务需求进行处理,例如如果是网络错误,可以提示用户检查网络连接;如果是响应状态码错误,可以提示用户检查输入的参数等。

案例一:用户登陆


目标

根据准备好的提示标签和样式,给用户反馈提示

讲解

  1. 需求:使用提前准备好的提示框,来把登录成功/失败结果提示给用户

    image-20230404104955330image-20230404105003019

  2. 使用提示框,反馈提示消息,因为有4处地方需要提示框,所以封装成函数

    1. 获取提示框

    2. 封装提示框函数,重复调用,满足提示需求

      功能:

      1. 显示提示框
      2. 不同提示文字msg,和成功绿色失败红色isSuccess参数(true成功,false失败)
      3. 过2秒后,让提示框自动消失
  3. 对应提示框核心代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 2.2 封装提示框函数,重复调用,满足提示需求
    * 功能:
    * 1. 显示提示框
    * 2. 不同提示文字msg,和成功绿色失败红色isSuccess(true成功,false失败)
    * 3. 过2秒后,让提示框自动消失
    */
    function alertFn(msg, isSuccess) {
    // 1> 显示提示框
    myAlert.classList.add('show')

    // 2> 实现细节
    myAlert.innerText = msg
    const bgStyle = isSuccess ? 'alert-success' : 'alert-danger'
    myAlert.classList.add(bgStyle)

    // 3> 过2秒隐藏
    setTimeout(() => {
    myAlert.classList.remove('show')
    // 提示:避免类名冲突,重置背景色
    myAlert.classList.remove(bgStyle)
    }, 2000)
    }

form-serialize 插件

form-serialize 是一个 JavaScript 库,用于将表单数据序列化为 URL 编码的字符串或 JSON 对象。使用 form-serialize 可以方便地将表单数据提交到服务器端进行处理。

下面是一个使用 form-serialize 库将表单数据序列化为 URL 编码字符串的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<form id="my-form">
<input type="text" name="username" value="test">
<input type="text" name="password" value="123456">
<input type="checkbox" name="remember" value="1" checked>
<button type="submit">提交</button>
</form>
<script src="form-serialize.min.js"></script>
<script>
const form = document.getElementById('my-form');
form.addEventListener('submit', function(event) {
event.preventDefault();
const data = serialize(form);
console.log(data); // 输出序列化后的数据
// 使用 axios 或其他方式提交数据到服务器端
});
</script>

在上面的代码中,我们首先引入了 form-serialize 库,并将表单数据序列化为 URL 编码字符串。在表单提交事件中,我们使用 serialize 函数将表单数据序列化为字符串,并将其输出到控制台中。最后,我们可以使用 axios 或其他方式将数据提交到服务器端进行处理。

需要注意的是,form-serialize 库还支持将表单数据序列化为 JSON 对象,可以根据实际情况进行选择。

希望这些信息对你有所帮助。

  1. 我们前面收集表单元素的值,是一个个标签获取的

    image-20230404105134538
  2. 如果一套表单里有很多很多表单元素,如何一次性快速收集出来呢?

    image-20230404105141226
  3. 使用 form-serialize 插件提供的 serialize 函数就可以办到

  4. form-serialize 插件语法:

    1. 引入 form-serialize 插件到自己网页中

    2. 使用 serialize 函数

      • 参数1:要获取的 form 表单标签对象(要求表单元素需要有 name 属性-用来作为收集的数据中属性名)

      • 参数2:配置对象

        • hash:
          • true - 收集出来的是一个 JS 对象结构
          • false - 收集出来的是一个查询字符串格式
        • empty:
          • true - 收集空值
          • false - 不收集空值
  5. 需求:收集登录表单里用户名和密码

  6. 对应代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    <!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>form-serialize插件使用</title>
    </head>

    <body>
    <form action="javascript:;" class="example-form">
    <input type="text" name="username">
    <br>
    <input type="text" name="password">
    <br>
    <input type="button" class="btn" value="提交">
    </form>
    <!--
    目标:在点击提交时,使用form-serialize插件,快速收集表单元素值
    1. 把插件引入到自己网页中
    -->
    <script src="./lib/form-serialize.js"></script>
    <script>
    document.querySelector('.btn').addEventListener('click', () => {
    /**
    * 2. 使用serialize函数,快速收集表单元素的值
    * 参数1:要获取哪个表单的数据
    * 表单元素设置name属性,值会作为对象的属性名
    * 建议name属性的值,最好和接口文档参数名一致
    * 参数2:配置对象
    * hash 设置获取数据结构
    * - true:JS对象(推荐)一般请求体里提交给服务器
    * - false: 查询字符串
    * empty 设置是否获取空值
    * - true: 获取空值(推荐)数据结构和标签结构一致
    * - false:不获取空值
    */
    const form = document.querySelector('.example-form')
    const data = serialize(form, { hash: true, empty: true })
    // const data = serialize(form, { hash: false, empty: true })
    // const data = serialize(form, { hash: true, empty: false })
    console.log(data)
    })
    </script>
    </body>

    </html>

案例

图书管理


目标

完成图书管理案例-图书列表数据渲染效果

讲解

  1. 需求:基于 axios 获取到图书列表数据,并用 JS 代码渲染数据,到准备好的模板标签中

    image-20230404110943200

  2. 步骤:

    1. 获取数据

    2. 渲染数据

      image-20230404110953752

      image-20230404111014560

  3. 获取数据的时候,需要给自己起一个外号,为什么需要给自己起一个外号呢?

    • 我们所有人数据都来自同一个服务器上,为了区分每个同学不同的数据,需要大家设置一个外号告诉服务器,服务器就会返回你对应的图书数据了
  4. 核心代码如下:

    因为默认展示列表,新增,修改,删除后都要重新获取并刷新列表,所以把获取数据渲染数据的代码封装在一个函数内,方便复用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    /**
    * 目标1:渲染图书列表
    * 1.1 获取数据
    * 1.2 渲染数据
    */
    const creator = '老张'
    // 封装-获取并渲染图书列表函数
    function getBooksList() {
    // 1.1 获取数据
    axios({
    url: 'http://hmajax.itheima.net/api/books',
    params: {
    // 外号:获取对应数据
    creator
    }
    }).then(result => {
    // console.log(result)
    const bookList = result.data.data
    // console.log(bookList)
    // 1.2 渲染数据
    const htmlStr = bookList.map((item, index) => {
    return `<tr>
    <td>${index + 1}</td>
    <td>${item.bookname}</td>
    <td>${item.author}</td>
    <td>${item.publisher}</td>
    <td data-id=${item.id}>
    <span class="del">删除</span>
    <span class="edit">编辑</span>
    </td>
    </tr>`
    }).join('')
    // console.log(htmlStr)
    document.querySelector('.list').innerHTML = htmlStr
    })
    }
    // 网页加载运行,获取并渲染列表一次
    getBooksList()

渲染列表

目标

完成图书管理案例-图书列表数据渲染效果

讲解
  1. 需求:基于 axios 获取到图书列表数据,并用 JS 代码渲染数据,到准备好的模板标签中

    image-20230404110943200

  2. 步骤:

    1. 获取数据

    2. 渲染数据

      image-20230404110953752

      image-20230404111014560

  3. 获取数据的时候,需要给自己起一个外号,为什么需要给自己起一个外号呢?

    • 我们所有人数据都来自同一个服务器上,为了区分每个同学不同的数据,需要大家设置一个外号告诉服务器,服务器就会返回你对应的图书数据了
  4. 核心代码如下:

    因为默认展示列表,新增,修改,删除后都要重新获取并刷新列表,所以把获取数据渲染数据的代码封装在一个函数内,方便复用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    /**
    * 目标1:渲染图书列表
    * 1.1 获取数据
    * 1.2 渲染数据
    */
    const creator = '老张'
    // 封装-获取并渲染图书列表函数
    function getBooksList() {
    // 1.1 获取数据
    axios({
    url: 'http://hmajax.itheima.net/api/books',
    params: {
    // 外号:获取对应数据
    creator
    }
    }).then(result => {
    // console.log(result)
    const bookList = result.data.data
    // console.log(bookList)
    // 1.2 渲染数据
    const htmlStr = bookList.map((item, index) => {
    return `<tr>
    <td>${index + 1}</td>
    <td>${item.bookname}</td>
    <td>${item.author}</td>
    <td>${item.publisher}</td>
    <td data-id=${item.id}>
    <span class="del">删除</span>
    <span class="edit">编辑</span>
    </td>
    </tr>`
    }).join('')
    // console.log(htmlStr)
    document.querySelector('.list').innerHTML = htmlStr
    })
    }
    // 网页加载运行,获取并渲染列表一次
    getBooksList()

渲染数据列表的2个步骤是什么? -> 获取数据,分析结构渲染到页面上

新增图书

目标

完成图书管理案例-新增图书需求

讲解
  1. 需求:点击添加按钮,出现准备好的新增图书弹框,填写图书信息提交到服务器保存,并更新图书列表

    image-20230404111235862

    image-20230404111251254

  2. 步骤:

    1. 新增弹框(控制显示和隐藏)(基于 Bootstrap 弹框和准备好的表单-用属性和 JS 方式控制)

    2. 在点击保存按钮时,收集数据&提交保存

    3. 刷新-图书列表)(重新调用下之前封装的获取并渲染列表的函数)

      image-20230404111343653

  3. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /**
    * 目标2:新增图书
    * 2.1 新增弹框->显示和隐藏
    * 2.2 收集表单数据,并提交到服务器保存
    * 2.3 刷新图书列表
    */
    // 2.1 创建弹框对象
    const addModalDom = document.querySelector('.add-modal')
    const addModal = new bootstrap.Modal(addModalDom)
    // 保存按钮->点击->隐藏弹框
    document.querySelector('.add-btn').addEventListener('click', () => {
    // 2.2 收集表单数据,并提交到服务器保存
    const addForm = document.querySelector('.add-form')
    const bookObj = serialize(addForm, { hash: true, empty: true })
    // console.log(bookObj)
    // 提交到服务器
    axios({
    url: 'http://hmajax.itheima.net/api/books',
    method: 'POST',
    data: {
    ...bookObj,
    creator
    }
    }).then(result => {
    // console.log(result)
    // 2.3 添加成功后,重新请求并渲染图书列表
    getBooksList()
    // 重置表单
    addForm.reset()
    // 隐藏弹框
    addModal.hide()
    })
    })

新增数据的3个步骤是什么? -> 准备好数据标签和样式,然后收集表单数据提交保存,刷新列表

删除图书

目标

完成图书管理案例-删除图书需求

讲解
  1. 需求:点击图书删除元素,删除当前图书数据

    image-20230404111530311

    image-20230404111546639

  2. 步骤:

    1. 给删除元素,绑定点击事件(事件委托方式并判断点击的是删除元素才走删除逻辑代码),并获取到要删除的数据id

    2. 基于 axios 和接口文档,调用删除接口,让服务器删除这条数据

    3. 重新获取并刷新图书列表

      image-20230404111612125

  3. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /**
    * 目标3:删除图书
    * 3.1 删除元素绑定点击事件->获取图书id
    * 3.2 调用删除接口
    * 3.3 刷新图书列表
    */
    // 3.1 删除元素->点击(事件委托)
    document.querySelector('.list').addEventListener('click', e => {
    // 获取触发事件目标元素
    // console.log(e.target)
    // 判断点击的是删除元素
    if (e.target.classList.contains('del')) {
    // console.log('点击删除元素')
    // 获取图书id(自定义属性id)
    const theId = e.target.parentNode.dataset.id
    // console.log(theId)
    // 3.2 调用删除接口
    axios({
    url: `http://hmajax.itheima.net/api/books/${theId}`,
    method: 'DELETE'
    }).then(() => {
    // 3.3 刷新图书列表
    getBooksList()
    })
    }
    })

删除数据的步骤是什么? -> 告知服务器要删除的数据id,服务器删除后,重新获取并刷新列表

编辑图书

目标

完成图书管理案例-编辑图书需求

讲解
  1. 因为编辑图书要做回显等,比较复杂,所以分了3个视频来讲解

  2. 需求:完成编辑图书回显当前图书数据到编辑表单,在用户点击修改按钮,收集数据提交到服务器保存,并刷新列表

    image-20230404111722254

  3. 编辑数据的核心思路:

    1. 给编辑元素,绑定点击事件(事件委托方式并判断点击的是编辑元素才走编辑逻辑代码),并获取到要编辑的数据id

    2. 基于 axios 和接口文档,调用查询图书详情接口,获取正在编辑的图书数据,并回显到表单中(页面上的数据是在用户的浏览器中不够准备,所以只要是查看数据都要从服务器获取)

      image-20230404111739153

    3. 收集并提交保存修改数据,并重新从服务器获取列表刷新页面

      image-20230404111756655

  4. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    /**
    * 目标4:编辑图书
    * 4.1 编辑弹框->显示和隐藏
    * 4.2 获取当前编辑图书数据->回显到编辑表单中
    * 4.3 提交保存修改,并刷新列表
    */
    // 4.1 编辑弹框->显示和隐藏
    const editDom = document.querySelector('.edit-modal')
    const editModal = new bootstrap.Modal(editDom)
    // 编辑元素->点击->弹框显示
    document.querySelector('.list').addEventListener('click', e => {
    // 判断点击的是否为编辑元素
    if (e.target.classList.contains('edit')) {
    // 4.2 获取当前编辑图书数据->回显到编辑表单中
    const theId = e.target.parentNode.dataset.id
    axios({
    url: `http://hmajax.itheima.net/api/books/${theId}`
    }).then(result => {
    const bookObj = result.data.data
    // document.querySelector('.edit-form .bookname').value = bookObj.bookname
    // document.querySelector('.edit-form .author').value = bookObj.author
    // 数据对象“属性”和标签“类名”一致
    // 遍历数据对象,使用属性去获取对应的标签,快速赋值
    const keys = Object.keys(bookObj) // ['id', 'bookname', 'author', 'publisher']
    keys.forEach(key => {
    document.querySelector(`.edit-form .${key}`).value = bookObj[key]
    })
    })
    editModal.show()
    }
    })
    // 修改按钮->点击->隐藏弹框
    document.querySelector('.edit-btn').addEventListener('click', () => {
    // 4.3 提交保存修改,并刷新列表
    const editForm = document.querySelector('.edit-form')
    const { id, bookname, author, publisher } = serialize(editForm, { hash: true, empty: true})
    // 保存正在编辑的图书id,隐藏起来:无需让用户修改
    // <input type="hidden" class="id" name="id" value="84783">
    axios({
    url: `http://hmajax.itheima.net/api/books/${id}`,
    method: 'PUT',
    data: {
    bookname,
    author,
    publisher,
    creator
    }
    }).then(() => {
    // 修改成功以后,重新获取并刷新列表
    getBooksList()

    // 隐藏弹框
    editModal.hide()
    })
    })

编辑数据的步骤是什么? -> 获取正在编辑数据并回显,收集编辑表单的数据提交保存,重新获取并刷新列表

总结

目标

总结下增删改查的核心思路

讲解
  1. 因为增删改查的业务在前端实际开发中非常常见,思路是可以通用的,所以总结下思路

    1.渲染列表(查)

    2.新增图书(增)

    3.删除图书(删)

    4.编辑图书(改)

    image-20230404111941722

  2. 渲染数据(查)

    核心思路:获取数据 -> 渲染数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 1.1 获取数据
    axios({...}).then(result => {
    const bookList = result.data.data
    // 1.2 渲染数据
    const htmlStr = bookList.map((item, index) => {
    return `<tr>
    <td>${index + 1}</td>
    <td>${item.bookname}</td>
    <td>${item.author}</td>
    <td>${item.publisher}</td>
    <td data-id=${item.id}>
    <span class="del">删除</span>
    <span class="edit">编辑</span>
    </td>
    </tr>`
    }).join('')
    document.querySelector('.list').innerHTML = htmlStr
    })
  3. 新增数据(增)

    核心思路:准备页面标签 -> 收集数据提交(必须) -> 刷新页面列表(可选)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 2.1 创建弹框对象
    const addModalDom = document.querySelector('.add-modal')
    const addModal = new bootstrap.Modal(addModalDom)
    document.querySelector('.add-btn').addEventListener('click', () => {
    // 2.2 收集表单数据,并提交到服务器保存
    const addForm = document.querySelector('.add-form')
    const bookObj = serialize(addForm, { hash: true, empty: true })
    axios({...}).then(result => {
    // 2.3 添加成功后,重新请求并渲染图书列表
    getBooksList()
    addForm.reset()
    addModal.hide()
    })
    })

    image-20230404112942935

  4. 删除图书(删)

    核心思路:绑定点击事件(获取要删除的图书唯一标识) -> 调用删除接口(让服务器删除此数据) -> 成功后重新获取并刷新列表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 3.1 删除元素->点击(事件委托)
    document.querySelector('.list').addEventListener('click', e => {
    if (e.target.classList.contains('del')) {
    // 获取图书id(自定义属性id)
    const theId = e.target.parentNode.dataset.id
    // 3.2 调用删除接口
    axios({...}).then(() => {
    // 3.3 刷新图书列表
    getBooksList()
    })
    }
    })

    image-20230404113338815

  5. 编辑图书(改)

    核心思路:准备编辑图书表单 -> 表单回显正在编辑的数据 -> 点击修改收集数据 -> 提交到服务器保存 -> 重新获取并刷新列表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // 4.1 编辑弹框->显示和隐藏
    const editDom = document.querySelector('.edit-modal')
    const editModal = new bootstrap.Modal(editDom)
    document.querySelector('.list').addEventListener('click', e => {
    if (e.target.classList.contains('edit')) {
    // 4.2 获取当前编辑图书数据->回显到编辑表单中
    const theId = e.target.parentNode.dataset.id
    axios({...}).then(result => {
    const bookObj = result.data.data
    // 遍历数据对象,使用属性去获取对应的标签,快速赋值
    const keys = Object.keys(bookObj)
    keys.forEach(key => {
    document.querySelector(`.edit-form .${key}`).value = bookObj[key]
    })
    })
    editModal.show()
    }
    })

    document.querySelector('.edit-btn').addEventListener('click', () => {
    // 4.3 提交保存修改,并刷新列表
    const editForm = document.querySelector('.edit-form')
    const { id, bookname, author, publisher } = serialize(editForm, { hash: true, empty: true})
    // 保存正在编辑的图书id,隐藏起来:无需让用户修改
    // <input type="hidden" class="id" name="id" value="84783">
    axios({...}).then(() => {
    getBooksList()
    editModal.hide()
    })
    })

    image-20230404113702515

个人信息


图片上传

  1. 什么是图片上传?

    • 就是把本地的图片上传到网页上显示
  2. 图片上传怎么做?

    • 先依靠文件选择元素获取用户选择的本地文件,接着提交到服务器保存,服务器会返回图片的 url 网址,然后把网址加载到 img 标签的 src 属性中即可显示
  3. 为什么不直接显示到浏览器上,要放到服务器上呢?

    • 因为浏览器保存是临时的,如果你想随时随地访问图片,需要上传到服务器上
  4. 图片上传怎么做呢?

    1. 先获取图片文件对象

    2. 使用 FormData 表单数据对象装入(因为图片是文件而不是以前的数字和字符串了所以传递文件一般需要放入 FormData 以键值对-文件流的数据传递(可以查看请求体-确认请求体结构)

      1
      2
      const fd = new FormData()
      fd.append(参数名, 值)
    3. 提交表单数据对象,使用服务器返回图片 url 网址

  5. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    <!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>图片上传</title>
    </head>

    <body>
    <!-- 文件选择元素 -->
    <input type="file" class="upload">
    <img src="" alt="" class="my-img">

    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
    /**
    * 目标:图片上传,显示到网页上
    * 1. 获取图片文件
    * 2. 使用 FormData 携带图片文件
    * 3. 提交到服务器,获取图片url网址使用
    */
    // 文件选择元素->change改变事件
    document.querySelector('.upload').addEventListener('change', e => {
    // 1. 获取图片文件
    console.log(e.target.files[0])
    // 2. 使用 FormData 携带图片文件
    const fd = new FormData()
    fd.append('img', e.target.files[0])
    // 3. 提交到服务器,获取图片url网址使用
    axios({
    url: 'http://hmajax.itheima.net/api/uploadimg',
    method: 'POST',
    data: fd
    }).then(result => {
    console.log(result)
    // 取出图片url网址,用img标签加载显示
    const imgUrl = result.data.data.url
    document.querySelector('.my-img').src = imgUrl
    })
    })
    </script>
    </body>

    </html>

    图片上传的思路

    —> 先用文件选择元素,获取到文件对象,然后装入 FormData 表单对象中,再发给服务器,得到图片在服务器的 URL 网址,再通过 img 标签加载图片显示

信息渲染

目标

把外号对应的用户信息渲染到页面上

讲解
  1. 需求:把外号对应的个人信息和头像,渲染到页面表单和头像标签上。

    image-20230404123708765
  2. 注意:还是需要准备一个外号,因为想要查看自己对应的用户信息,不想被别人影响

  3. 步骤:

    • 获取数据
    • 渲染数据到页面
  4. 代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /**
    * 目标1:信息渲染
    * 1.1 获取用户的数据
    * 1.2 回显数据到标签上
    * */
    const creator = '播仔'
    // 1.1 获取用户的数据
    axios({
    url: 'http://hmajax.itheima.net/api/settings',
    params: {
    creator
    }
    }).then(result => {
    const userObj = result.data.data
    // 1.2 回显数据到标签上
    Object.keys(userObj).forEach(key => {
    if (key === 'avatar') {
    // 赋予默认头像
    document.querySelector('.prew').src = userObj[key]
    } else if (key === 'gender') {
    // 赋予默认性别
    // 获取性别单选框:[男radio元素,女radio元素]
    const gRadioList = document.querySelectorAll('.gender')
    // 获取性别数字:0男,1女
    const gNum = userObj[key]
    // 通过性别数字,作为下标,找到对应性别单选框,设置选中状态
    gRadioList[gNum].checked = true
    } else {
    // 赋予默认内容
    document.querySelector(`.${key}`).value = userObj[key]
    }
    })
    })

渲染数据和图书列表的渲染思路是否一样呢,是什么? -> 一样的,都是获取到数据,然后渲染到页面上

头像修改

目标

修改用户的头像并立刻生效

讲解
  1. 需求:点击修改用户头像

    image-20230404124524401

  2. 实现步骤如下:

    1. 获取到用户选择的头像文件

    2. 调用头像修改接口,并除了头像文件外,还要在 FormData 表单数据对象中携带外号

    3. 提交到服务器保存此用户对应头像文件,并把返回的头像图片 url 网址设置在页面上

      image-20230404124540629

  3. 注意:重新刷新重新获取,已经是修改后的头像了(证明服务器那边确实保存成功)

  4. 核心代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 目标2:修改头像
    * 2.1 获取头像文件
    * 2.2 提交服务器并更新头像
    * */
    // 文件选择元素->change事件
    document.querySelector('.upload').addEventListener('change', e => {
    // 2.1 获取头像文件
    console.log(e.target.files[0])
    const fd = new FormData()
    fd.append('avatar', e.target.files[0])
    fd.append('creator', creator)
    // 2.2 提交服务器并更新头像
    axios({
    url: 'http://hmajax.itheima.net/api/avatar',
    method: 'PUT',
    data: fd
    }).then(result => {
    const imgUrl = result.data.data.avatar
    // 把新的头像回显到页面上
    document.querySelector('.prew').src = imgUrl
    })
    })

为什么这次上传头像,需要携带外号呢? -> 因为这次头像到后端,是要保存在某个用户名下的,所以要把外号名字一起携带过去

信息修改

目标

把用户修改的信息提交到服务器保存

讲解
  1. 需求:点击提交按钮,收集个人信息,提交到服务器保存(无需重新获取刷新,因为页面已经是最新的数据了)

    1. 收集表单数据

    2. 提交到服务器保存-调用用户信息更新接口(注意请求方法是 PUT)代表数据更新的意思

      image-20230404125310049
  2. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
    * 目标3:提交表单
    * 3.1 收集表单信息
    * 3.2 提交到服务器保存
    */
    // 保存修改->点击
    document.querySelector('.submit').addEventListener('click', () => {
    // 3.1 收集表单信息
    const userForm = document.querySelector('.user-form')
    const userObj = serialize(userForm, { hash: true, empty: true })
    userObj.creator = creator
    // 性别数字字符串,转成数字类型
    userObj.gender = +userObj.gender
    console.log(userObj)
    // 3.2 提交到服务器保存
    axios({
    url: 'http://hmajax.itheima.net/api/settings',
    method: 'PUT',
    data: userObj
    }).then(result => {
    })
    })

信息修改数据和以前增删改查哪个实现的思路比较接近呢?

-> 编辑,首先回显已经做完了,然后收集用户最新改动后的数据,提交到服务器保存,因为页面最终就是用户刚写的数据,所以不用重新获取并刷新页面了

提示框

目标

把用户更新个人信息结果,用提示框反馈给用户

讲解
  1. 需求:使用 bootstrap 提示框,提示个人信息设置后的结果

    image-20230404125517679
  2. bootstrap 的 toast 提示框和 modal 弹框使用很像,语法如下:

    1. 先准备对应的标签结构(模板里已有)

    2. 设置延迟自动消失的时间

      1
      2
      3
      <div class="toast" data-bs-delay="1500">
      提示框内容
      </div>
    3. 使用 JS 的方式,在 axios 请求响应成功时,展示结果

      1
      2
      3
      4
      5
      6
      // 创建提示框对象
      const toastDom = document.querySelector('css选择器')
      const toast = new bootstrap.Toast(toastDom)

      // 显示提示框
      toast.show()
  3. 核心代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /**
    * 目标3:提交表单
    * 3.1 收集表单信息
    * 3.2 提交到服务器保存
    */
    /**
    * 目标4:结果提示
    * 4.1 创建toast对象
    * 4.2 调用show方法->显示提示框
    */
    // 保存修改->点击
    document.querySelector('.submit').addEventListener('click', () => {
    // 3.1 收集表单信息
    const userForm = document.querySelector('.user-form')
    const userObj = serialize(userForm, { hash: true, empty: true })
    userObj.creator = creator
    // 性别数字字符串,转成数字类型
    userObj.gender = +userObj.gender
    console.log(userObj)
    // 3.2 提交到服务器保存
    axios({
    url: 'http://hmajax.itheima.net/api/settings',
    method: 'PUT',
    data: userObj
    }).then(result => {
    // 4.1 创建toast对象
    const toastDom = document.querySelector('.my-toast')
    const toast = new bootstrap.Toast(toastDom)

    // 4.2 调用show方法->显示提示框
    toast.show()
    })
    })

bootstrap 弹框什么时候用 JS 方式控制显示呢

-> 需要执行一些其他的 JS 逻辑后,再去显示/隐藏弹框时

Ajax 原理

Ajax(Asynchronous JavaScript and XML)是一种用于创建交互式 Web 应用程序的技术。它允许在不重新加载整个页面的情况下,通过异步请求和响应数据来更新部分页面内容。

Ajax 的原理基于以下几个步骤:

  1. 用户与页面交互,触发事件,例如点击按钮、输入文本等。
  2. JavaScript 代码通过 XMLHttpRequest 对象创建一个 HTTP 请求并发送给服务器。该请求包含需要发送的数据,例如表单数据、查询字符串等。
  3. 服务器端处理请求,可以是从数据库中获取数据、执行某些计算等,并将响应数据返回给客户端。响应数据可以是 XML、JSON、HTML 或纯文本等格式。
  4. 客户端收到响应数据后,JavaScript 代码解析数据并使用 DOM 操作将数据插入到页面中。这些操作可以是添加、删除或修改页面元素。

XMLHttpRequest 基础使用


  1. AJAX 是浏览器与服务器通信的技术,采用 XMLHttpRequest 对象相关代码

  2. axios 是对 XHR 相关代码进行了封装,让我们只关心传递的接口参数

  3. 学习 XHR 也是了解 axios 内部与服务器交互过程的真正原理

    image-20230221182835545
  4. 语法如下:

    1
    2
    3
    4
    5
    6
    7
    const xhr = new XMLHttpRequest()
    xhr.open('请求方法', '请求url网址')
    xhr.addEventListener('loadend', () => {
    // 响应结果
    console.log(xhr.response)
    })
    xhr.send()
    image-20230221183057392
  5. 需求:以一个需求来体验下原生 XHR 语法,获取所有省份列表并展示到页面上

  6. 代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    <!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>XMLHttpRequest_基础使用</title>
    </head>

    <body>
    <p class="my-p"></p>
    <script>
    /**
    * 目标:使用XMLHttpRequest对象与服务器通信
    * 1. 创建 XMLHttpRequest 对象
    * 2. 配置请求方法和请求 url 地址
    * 3. 监听 loadend 事件,接收响应结果
    * 4. 发起请求
    */
    // 1. 创建 XMLHttpRequest 对象
    const xhr = new XMLHttpRequest()

    // 2. 配置请求方法和请求 url 地址
    xhr.open('GET', 'http://hmajax.itheima.net/api/province')

    // 3. 监听 loadend 事件,接收响应结果
    xhr.addEventListener('loadend', () => {
    console.log(xhr.response)
    const data = JSON.parse(xhr.response)
    console.log(data.list.join('<br>'))
    document.querySelector('.my-p').innerHTML = data.list.join('<br>')
    })

    // 4. 发起请求
    xhr.send()
    </script>
    </body>

    </html>
  • How it works
    • window 提供的 XMLHttpRequest
  • why learn it
    • 有更多与服务器通信的方式
    • 了解 axios 的内部原理
  • 使用步骤
    1. 创建 XHR 对象
    2. 调用 open 方法,设置 url 和请求方法
    3. 监听 loadend 事件,接收结果
    4. 调用 send 方法,发起请求

查询参数


  1. 什么是查询参数:携带额外信息给服务器,返回匹配想要的数据

  2. 查询参数原理要携带的位置和语法:http://xxxx.com/xxx/xxx?参数名1=值1&参数名2=值2

  3. 所以,原生 XHR 需要自己在 url 后面携带查询参数字符串,没有 axios 帮助我们把 params 参数拼接到 url 字符串后面了

  4. 需求:查询河北省下属的城市列表

    image-20230404133429378

  5. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /**
    * 目标:使用XHR携带查询参数,展示某个省下属的城市列表
    */
    const xhr = new XMLHttpRequest()
    xhr.open('GET', 'http://hmajax.itheima.net/api/city?pname=辽宁省')
    xhr.addEventListener('loadend', () => {
    console.log(xhr.response)
    const data = JSON.parse(xhr.response)
    console.log(data)
    document.querySelector('.city-p').innerHTML = data.list.join('<br>')
    })
    xhr.send()

XHR 如何携带查询参数

-> 在调用 open 方法的时候,在 url? 后面按照指定格式拼接参数名和值

案例

目标

使用 XHR 完成案例地区查询

讲解
  1. 需求:和我们之前做的类似,就是不用 axios 而是用 XHR 实现,输入省份和城市名字后,点击查询,传递多对查询参数并获取地区列表的需求

    image-20230221184135458

  2. 但是多个查询参数,如果自己拼接很麻烦,这里用 URLSearchParams 把参数对象转成“参数名=值&参数名=值“格式的字符串,语法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 1. 创建 URLSearchParams 对象
    const paramsObj = new URLSearchParams({
    参数名1: 值1,
    参数名2: 值2
    })

    // 2. 生成指定格式查询参数字符串
    const queryString = paramsObj.toString()
    // 结果:参数名1=值1&参数名2=值2

JS 对象如何转成 查询参数格式字符串

——> 使用 URLSearchParams ,然后使用 toString 方法 在 open 里拼接

数据提交


目标

通过 XHR 提交用户名和密码,完成注册功能

讲解

  1. 了解原生 XHR 进行数据提交的方式

  2. 需求:通过 XHR 完成注册用户功能

    image-20230404135245271

  3. 步骤和语法:

    1. 注意1:但是这次没有 axios 帮我们了,我们需要自己设置请求头 Content-Type:application/json,来告诉服务器端,我们发过去的内容类型是 JSON 字符串,让他转成对应数据结构取值使用

    2. 注意2:没有 axios 了,我们前端要传递的请求体数据,也没人帮我把 JS 对象转成 JSON 字符串了,需要我们自己转换

    3. 注意3:原生 XHR 需要在 send 方法调用时,传入请求体携带

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      const xhr = new XMLHttpRequest()
      xhr.open('请求方法', '请求url网址')
      xhr.addEventListener('loadend', () => {
      console.log(xhr.response)
      })

      // 1. 告诉服务器,我传递的内容类型,是 JSON 字符串
      xhr.setRequestHeader('Content-Type', 'application/json')
      // 2. 准备数据并转成 JSON 字符串
      const user = { username: 'itheima007', password: '7654321' }
      const userStr = JSON.stringify(user)
      // 3. 发送请求体数据
      xhr.send(userStr)
  4. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * 目标:使用xhr进行数据提交-完成注册功能
    */
    document.querySelector('.reg-btn').addEventListener('click', () => {
    const xhr = new XMLHttpRequest()
    xhr.open('POST', 'http://hmajax.itheima.net/api/register')
    xhr.addEventListener('loadend', () => {
    console.log(xhr.response)
    })

    // 设置请求头-告诉服务器内容类型(JSON字符串)
    xhr.setRequestHeader('Content-Type', 'application/json')
    // 准备提交的数据
    const userObj = {
    username: 'itheima007',
    password: '7654321'
    }
    const userStr = JSON.stringify(userObj)
    // 设置请求体,发起请求
    xhr.send(userStr)
    })

XHR 如何提交请求体数据

——> 在 send 中 携带请求体参数,按照后端要求的内容类型携带

Promise


Promise 是 JavaScript 中处理异步操作的一种方法,它可以让异步操作更加简单和易于管理。Promise 是一个对象,表示一个尚未完成但将来会完成的操作,它可以是成功的、失败的或者处于等待状态。

Promise 对象具有以下三种状态:

  1. Pending(进行中):初始状态,表示操作还没有完成。

  2. Fulfilled(已完成):表示操作已经成功完成,Promise 对象返回一个值。

  3. Rejected(已失败):表示操作已经失败,Promise 对象返回一个错误信息。

Promise 对象具有以下三个方法:

  1. then():用于指定 Promise 对象的成功和失败的回调函数。

  2. catch():用于指定 Promise 对象的失败的回调函数。

  3. finally():用于指定 Promise 对象的完成的回调函数,无论 Promise 是成功还是失败,都会执行。

使用 Promise 可以带来以下几个优点:

  1. 更加清晰:Promise 可以使异步操作更加清晰和易于管理,避免了回调函数的嵌套。

  2. 更加可靠:Promise 对象具有状态,可以避免回调函数的多次调用和状态的不确定性,使得异步操作更加可靠。

  3. 更加灵活:Promise 可以链式调用,允许在多个异步操作之间进行组合和串联。

Promise 对象并不是适用于所有的异步操作,有些情况下使用回调函数可以更加简单和直观。

Promise 入门

quick start

Promise 是一个对象,它代表了一个 异步操作最终完成或者失败。因为大多数人仅仅是使用已创建的 Promise 实例对象,所以首先说明怎样使用 Promise,再说明如何创建 Promise。

本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。

假设现在有一个名为 createAudioFileAsync() 的函数,它接收一些配置和两个回调函数,然后异步地生成音频文件。一个回调函数在文件成功创建时被调用,另一个则在出现异常时被调用。

以下为使用 createAudioFileAsync() 的示例:

1
2
3
4
5
6
7
8
9
10
11
// 成功的回调函数
function successCallback(result) {
console.log("音频文件创建成功:" + result);
}

// 失败的回调函数
function failureCallback(error) {
console.log("音频文件创建失败:" + error);
}

createAudioFileAsync(audioSettings, successCallback, failureCallback)

更现代的函数会返回一个 Promise 对象,使得你可以将你的回调函数绑定在该 Promise 上。

如果函数 createAudioFileAsync() 被重写为返回 Promise 的形式,那么我们可以像下面这样简单地调用它:

1
2
const promise = createAudioFileAsync(audioSettings);
promise.then(successCallback, failureCallback);

或者简写为:

1
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);

这个称为 异步函数调用

约定

在使用 Promise 时,需要遵守以下 约定

  • 在 本轮 事件循环 运行完成之前,回调函数是不会被调用的。
    • 事件循环 Event Loop —— JavaScript 运行时中处理异步操作的机制
    • 当 Promise 中的异步操作完成后,它会将结果放入一个任务队列中,然后等待在任务队列中的回调函数被执行。事件循环会不断地从任务队列中取出待执行的回调函数,并执行它们,直到任务队列为空。
    • 在 JavaScript 中,每个事件循环由两个阶段组成:执行同步代码和执行异步代码。在执行异步代码阶段,JavaScript 引擎会从任务队列中取出待执行的回调函数,并执行它们。在执行完所有的异步代码之后,JavaScript 引擎会进入下一个事件循环
    • 因此,在本轮事件循环中注册的回调函数,只有在下一轮事件循环开始时才会被执行。如果回调函数需要等待某个异步操作完成才能执行,那么它会被放入任务队列中,等待 JavaScript 引擎在下一轮事件循环中执行。
    • 如果某个回调函数的执行时间过长,则可能会导致线程被阻塞,从而影响应用程序的性能。所以 应尽量的保证回调函数的执行时间短,避免出现阻塞
  • 即使 异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用
  • 通过多次调用 then() 可以添加多个 回调函数,他们会按照 插入顺序进行执行
概览
  • 什么是 Promise ?

    • Promise 对象表示异步操作最终的完成(或失败)以及其结果值。
    • 一个 Promise 是一个代理,它代表一个在创建 promise 时不一定已知的值。它允许你将处理程序与异步操作的最终成功值或失败原因关联起来。这使得异步方法可以像同步方法一样返回值:异步方法不会立即返回最终值,而是返回一个 promise,以便在将来的某个时间点提供该值。
    • 一个待定的 Promise 最终状态 可以是 已兑现 并返回一个值,或者是 已拒绝 并返回一个原因(错误)。当其中任意一种情况发生时,通过 Promise 的 then 方法串联的处理程序将被调用。如果绑定相应处理程序时 Promise 已经兑现或拒绝,这处理程序将被立即调用,因此在异步操作完成和绑定处理程序之间不存在竞态条件。
  • Promise 的好处是什么?

    • 逻辑更清晰(成功或失败会关联后续的处理函数)

    • 了解 axios 函数内部运作的机制

      image-20230222113651404

    • 能解决回调函数地狱问题(后面会讲到)

  • Promise 管理异步任务,语法怎么用?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 1. 创建 Promise 对象
    const p = new Promise((resolve, reject) => {
    // 2. 执行异步任务-并传递结果
    // 成功调用: resolve(值) 触发 then() 执行
    // 失败调用: reject(值) 触发 catch() 执行
    })
    // 3. 接收结果
    p.then(result => {
    // 成功
    }).catch(error => {
    // 失败
    })
  • 示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * 目标:使用Promise管理异步任务
    */
    // 1. 创建Promise对象
    const p = new Promise((resolve, reject) => {
    // 2. 执行异步代码
    setTimeout(() => {
    // resolve('模拟AJAX请求-成功结果')
    reject(new Error('模拟AJAX请求-失败结果'))
    }, 2000)
    })

    // 3. 获取结果
    p.then(result => {
    console.log(result)
    }).catch(error => {
    console.log(error)
    })
  • promise的使用步骤

    1. new Promise() 对象执行异步任务
    2. resolve 关联 then 的 回调函数 传递成功结果
    3. reject 关联 catch 的回调函数 传递 失败结果

Promise 的状态

一个 Promise 必然处于以下几种状态之一:

  • 待定( pending :初始状态,既没有被兑现,也没有被拒绝。
  • 已兑现( fulfilled:意味着操作成功完成。
  • 已拒绝( rejected:意味着操作失败。

流程图展示了 Promise 状态在 pending、fulfilled 和 rejected 之间如何通过 then() 和 catch() 处理程序进行转换。一个待定的 Promise 可以变成已兑现或已拒绝的状态。如果 Promise 已经兑现,则会执行“on fulfillment”处理程序(即 then() 方法的第一个参数),并继续执行进一步的异步操作。如果 Promise 被拒绝,则会执行错误处理程序,可以将其作为 then() 方法的第二个参数或 catch() 方法的唯一参数来传递。

Promise 的状态改变的作用:调用对应函数,改变 Promise 对象状态后,内部触发对应回调函数传参并执行

image-20230222120815484

注意:每个 Promise 对象一旦被兑现/拒绝,那就是已敲定了,状态无法再被改变

案例

  1. 需求:使用 Promise 和 XHR 请求省份列表数据并展示到页面上

    image-20230404140252181

  2. 步骤:

    1. 创建 Promise 对象

    2. 执行 XHR 异步代码,获取省份列表数据

    3. 关联成功或失败回调函数,做后续的处理

      错误情况:用地址错了404演示

  3. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /**
    * 目标:使用Promise管理XHR请求省份列表
    * 1. 创建Promise对象
    * 2. 执行XHR异步代码,获取省份列表
    * 3. 关联成功或失败函数,做后续处理
    */
    // 1. 创建Promise对象
    const p = new Promise((resolve, reject) => {
    // 2. 执行XHR异步代码,获取省份列表
    const xhr = new XMLHttpRequest()
    xhr.open('GET', 'http://hmajax.itheima.net/api/province')
    xhr.addEventListener('loadend', () => {
    // xhr如何判断响应成功还是失败的?
    // 2xx开头的都是成功响应状态码
    if (xhr.status >= 200 && xhr.status < 300) {
    resolve(JSON.parse(xhr.response))
    } else {
    reject(new Error(xhr.response))
    }
    })
    xhr.send()
    })

    // 3. 关联成功或失败函数,做后续处理
    p.then(result => {
    console.log(result)
    document.querySelector('.my-p').innerHTML = result.list.join('<br>')
    }).catch(error => {
    // 错误对象要用console.dir详细打印
    console.dir(error)
    // 服务器返回错误提示消息,插入到p标签显示
    document.querySelector('.my-p').innerHTML = error.message
    })

AJAX 如何判断 请求是否响应成功: 响应状态码在 [200,300) 之间

封装简易 axios

案例一:获取省份列表
  1. 需求:基于 Promise 和 XHR 封装 myAxios 函数,获取省份列表展示到页面

    image-20230222130217597

  2. 核心语法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function myAxios(config) {
    return new Promise((resolve, reject) => {
    // XHR 请求
    // 调用成功/失败的处理程序
    })
    }

    myAxios({
    url: '目标资源地址'
    }).then(result => {

    }).catch(error => {

    })
  3. 步骤:

    1. 定义 myAxios 函数,接收配置对象,返回 Promise 对象
    2. 发起 XHR 请求,默认请求方法为 GET
    3. 调用成功/失败的处理程序
    4. 使用 myAxios 函数,获取省份列表展示
  4. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    /**
    * 目标:封装_简易axios函数_获取省份列表
    * 1. 定义myAxios函数,接收配置对象,返回Promise对象
    * 2. 发起XHR请求,默认请求方法为GET
    * 3. 调用成功/失败的处理程序
    * 4. 使用myAxios函数,获取省份列表展示
    */
    // 1. 定义myAxios函数,接收配置对象,返回Promise对象
    function myAxios(config) {
    return new Promise((resolve, reject) => {
    // 2. 发起XHR请求,默认请求方法为GET
    const xhr = new XMLHttpRequest()
    xhr.open(config.method || 'GET', config.url)
    xhr.addEventListener('loadend', () => {
    // 3. 调用成功/失败的处理程序
    if (xhr.status >= 200 && xhr.status < 300) {
    resolve(JSON.parse(xhr.response))
    } else {
    reject(new Error(xhr.response))
    }
    })
    xhr.send()
    })
    }

    // 4. 使用myAxios函数,获取省份列表展示
    myAxios({
    url: 'http://hmajax.itheima.net/api/province'
    }).then(result => {
    console.log(result)
    document.querySelector('.my-p').innerHTML = result.list.join('<br>')
    }).catch(error => {
    console.log(error)
    document.querySelector('.my-p').innerHTML = error.message
    })

自己封装的 myAxios 如何设置默认请求方法 GET ——> config.method 判断有值就用,无值用‘GET’方法

案例二:获取地区列表

修改 myAxios 函数支持传递查询参数,获取辽宁省,大连市的地区列表

  1. 需求:在上个封装的建议 axios 函数基础上,修改代码支持传递查询参数功能

  2. 修改步骤:

    1. myAxios 函数调用后,判断 params 选项
    2. 基于 URLSearchParams 转换查询参数字符串
    3. 使用自己封装的 myAxios 函数显示地区列表
  3. 核心代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    function myAxios(config) {
    return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    // 1. 判断有params选项,携带查询参数
    if (config.params) {
    // 2. 使用URLSearchParams转换,并携带到url上
    const paramsObj = new URLSearchParams(config.params)
    const queryString = paramsObj.toString()
    // 把查询参数字符串,拼接在url?后面
    config.url += `?${queryString}`
    }

    xhr.open(config.method || 'GET', config.url)
    xhr.addEventListener('loadend', () => {
    if (xhr.status >= 200 && xhr.status < 300) {
    resolve(JSON.parse(xhr.response))
    } else {
    reject(new Error(xhr.response))
    }
    })
    xhr.send()
    })
    }

    // 3. 使用myAxios函数,获取地区列表
    myAxios({
    url: 'http://hmajax.itheima.net/api/area',
    params: {
    pname: '辽宁省',
    cname: '大连市'
    }
    }).then(result => {
    console.log(result)
    document.querySelector('.my-p').innerHTML = result.list.join('<br>')
    })

外面传入查询参数对象,myAxios 函数内如何转查询参数字符串 —— > 使用 URLSearchParams 对象转换

案例三:注册用户

修改 myAxios 函数支持传递请求体数据,完成注册用户

  1. 需求:修改 myAxios 函数支持传递请求体数据,完成注册用户功能

  2. 修改步骤:

    1. myAxios 函数调用后,判断 data 选项
    2. 转换数据类型,在 send 方法中发送
    3. 使用自己封装的 myAxios 函数完成注册用户功能
  3. 核心代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    function myAxios(config) {
    return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()

    if (config.params) {
    const paramsObj = new URLSearchParams(config.params)
    const queryString = paramsObj.toString()
    config.url += `?${queryString}`
    }
    xhr.open(config.method || 'GET', config.url)

    xhr.addEventListener('loadend', () => {
    if (xhr.status >= 200 && xhr.status < 300) {
    resolve(JSON.parse(xhr.response))
    } else {
    reject(new Error(xhr.response))
    }
    })
    // 1. 判断有data选项,携带请求体
    if (config.data) {
    // 2. 转换数据类型,在send中发送
    const jsonStr = JSON.stringify(config.data)
    xhr.setRequestHeader('Content-Type', 'application/json')
    xhr.send(jsonStr)
    } else {
    // 如果没有请求体数据,正常的发起请求
    xhr.send()
    }
    })
    }

    document.querySelector('.reg-btn').addEventListener('click', () => {
    // 3. 使用myAxios函数,完成注册用户
    myAxios({
    url: 'http://hmajax.itheima.net/api/register',
    method: 'POST',
    data: {
    username: 'itheima999',
    password: '666666'
    }
    }).then(result => {
    console.log(result)
    }).catch(error => {
    console.dir(error)
    })
    })

外面传入 data 选项,myAxios 函数内如何携带请求体参数 ——> 判断外面传入了这个属性,自己转成 JSON 字符串并设置请求头并在 send 方法中携带

案例:天气预报


默认数据

把北京市的数据,填充到页面默认显示

image-20230222133327806

  1. 步骤

    1. 先获取北京市天气预报,展示
    2. 搜索城市列表,展示
    3. 点击城市,切换显示对应天气数据
  2. 先封装函数,获取城市天气并设置页面内容

  3. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    /**
    * 目标1:默认显示-北京市天气
    * 1.1 获取北京市天气数据
    * 1.2 数据展示到页面
    */
    // 获取并渲染城市天气函数
    function getWeather(cityCode) {
    // 1.1 获取北京市天气数据
    myAxios({
    url: 'http://hmajax.itheima.net/api/weather',
    params: {
    city: cityCode
    }
    }).then(result => {
    console.log(result)
    const wObj = result.data
    // 1.2 数据展示到页面
    // 阳历和农历日期
    const dateStr = `<span class="dateShort">${wObj.date}</span>
    <span class="calendar">农历&nbsp;
    <span class="dateLunar">${wObj.dateLunar}</span>
    </span>`
    document.querySelector('.title').innerHTML = dateStr
    // 城市名字
    document.querySelector('.area').innerHTML = wObj.area
    // 当天气温
    const nowWStr = `<div class="tem-box">
    <span class="temp">
    <span class="temperature">${wObj.temperature}</span>
    <span>°</span>
    </span>
    </div>
    <div class="climate-box">
    <div class="air">
    <span class="psPm25">${wObj.psPm25}</span>
    <span class="psPm25Level">${wObj.psPm25Level}</span>
    </div>
    <ul class="weather-list">
    <li>
    <img src="${wObj.weatherImg}" class="weatherImg" alt="">
    <span class="weather">${wObj.weather}</span>
    </li>
    <li class="windDirection">${wObj.windDirection}</li>
    <li class="windPower">${wObj.windPower}</li>
    </ul>
    </div>`
    document.querySelector('.weather-box').innerHTML = nowWStr
    // 当天天气
    const twObj = wObj.todayWeather
    const todayWStr = `<div class="range-box">
    <span>今天:</span>
    <span class="range">
    <span class="weather">${twObj.weather}</span>
    <span class="temNight">${twObj.temNight}</span>
    <span>-</span>
    <span class="temDay">${twObj.temDay}</span>
    <span>℃</span>
    </span>
    </div>
    <ul class="sun-list">
    <li>
    <span>紫外线</span>
    <span class="ultraviolet">${twObj.ultraviolet}</span>
    </li>
    <li>
    <span>湿度</span>
    <span class="humidity">${twObj.humidity}</span>%
    </li>
    <li>
    <span>日出</span>
    <span class="sunriseTime">${twObj.sunriseTime}</span>
    </li>
    <li>
    <span>日落</span>
    <span class="sunsetTime">${twObj.sunsetTime}</span>
    </li>
    </ul>`
    document.querySelector('.today-weather').innerHTML = todayWStr

    // 7日天气预报数据展示
    const dayForecast = wObj.dayForecast
    const dayForecastStr = dayForecast.map(item => {
    return `<li class="item">
    <div class="date-box">
    <span class="dateFormat">${item.dateFormat}</span>
    <span class="date">${item.date}</span>
    </div>
    <img src="${item.weatherImg}" alt="" class="weatherImg">
    <span class="weather">${item.weather}</span>
    <div class="temp">
    <span class="temNight">${item.temNight}</span>-
    <span class="temDay">${item.temDay}</span>
    <span>℃</span>
    </div>
    <div class="wind">
    <span class="windDirection">${item.windDirection}</span>
    <span class="windPower">${item.windPower}</span>
    </div>
    </li>`
    }).join('')
    // console.log(dayForecastStr)
    document.querySelector('.week-wrap').innerHTML = dayForecastStr
    })
    }

    // 默认进入网页-就要获取天气数据(北京市城市编码:'110100')
    getWeather('110100')

搜索城市列表

根据关键字,展示匹配的城市列表

  1. 效果:搜索匹配关键字相关城市名字,展示城市列表即可

    image-20230222133553010
  2. 步骤

    1. 绑定 input 事件,获取关键字
    2. 获取展示城市列表数据
  3. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 目标2:搜索城市列表
    * 2.1 绑定input事件,获取关键字
    * 2.2 获取展示城市列表数据
    */
    // 2.1 绑定input事件,获取关键字
    document.querySelector('.search-city').addEventListener('input', (e) => {
    console.log(e.target.value)
    // 2.2 获取展示城市列表数据
    myAxios({
    url: 'http://hmajax.itheima.net/api/weather/city',
    params: {
    city: e.target.value
    }
    }).then(result => {
    console.log(result)
    const liStr = result.data.map(item => {
    return `<li class="city-item" data-code="${item.code}">${item.name}</li>`
    }).join('')
    console.log(liStr)
    document.querySelector('.search-list').innerHTML = liStr
    })
    })

监听输入框实时改变的事件是什么 ——> input 事件

展示城市天气

点击搜索框列表城市名字,切换对应城市天气数据

  1. 效果:点击城市列表名字,切换当前页面天气数据

    image-20230222134653884

  2. 步骤

    1. 检测搜索列表点击事件,获取城市 code 值
    2. 复用获取展示城市天气函数
  3. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * 目标3:切换城市天气
    * 3.1 绑定城市点击事件,获取城市code值
    * 3.2 调用获取并展示天气的函数
    */
    // 3.1 绑定城市点击事件,获取城市code值
    document.querySelector('.search-list').addEventListener('click', e => {
    if (e.target.classList.contains('city-item')) {
    // 只有点击城市li才会走这里
    const cityCode = e.target.dataset.code
    console.log(cityCode)
    // 3.2 调用获取并展示天气的函数
    getWeather(cityCode)
    }
    })

Ajax 进阶

同步代码和异步代码

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

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

  3. 回答代码打印顺序:发现异步代码接收结果,使用的都是回调函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const result = 0 + 1
    console.log(result)
    setTimeout(() => {
    console.log(2)
    }, 2000)
    document.querySelector('.btn').addEventListener('click', () => {
    console.log(3)
    })
    document.body.style.backgroundColor = 'pink'
    console.log(4)

    结果:1, 4, 2

    按钮点击一次打印一次 3

在 JavaScript 中,同步代码和异步代码是指代码执行的方式和顺序不同的两种类型的代码。同步代码是指按照代码的顺序依次执行的代码,每个代码块执行完毕后才会执行下一个代码块。异步代码是指不按照代码顺序执行的代码,异步代码的执行顺序和时间是由事件循环(Event Loop)控制的。

  1. 同步代码:
1
2
3
4
5
6
7
8
console.log("start"); // 同步代码
let result = add(1, 2); // 同步代码
console.log(result); // 同步代码
console.log("end"); // 同步代码

function add(a, b) {
return a + b;
}

在上面的代码中,所有代码块都是同步代码,按照顺序依次执行。

  1. 异步代码:
1
2
3
4
5
console.log("start"); // 同步代码
setTimeout(() => {
console.log("Hello, world!"); // 异步代码
}, 1000);
console.log("end"); // 同步代码

在上面的代码中,setTimeout() 方法是异步代码,它会在 1000 毫秒后执行回调函数。因此,在执行 setTimeout() 方法时,JavaScript 引擎会立即执行下一行代码,不会等待回调函数的执行。

在异步代码中,回调函数的执行顺序和时间是由事件循环控制的。当异步操作完成后,回调函数会被添加到任务队列中,等待 JavaScript 引擎执行。当任务队列中没有任务时,JavaScript 引擎会进入下一轮事件循环。

回调函数地狱


回调函数地狱(Callback Hell)是指在编写异步 JavaScript 代码时,由于多层嵌套的回调函数导致代码结构复杂、难以理解和维护的现象。它通常出现在需要进行多个异步操作的场景中,例如 AJAX 请求、文件读取等。

回调函数地狱的问题在于,多层嵌套的回调函数使得代码结构复杂,难以理解和维护。同时,由于回调函数的执行顺序不确定,可能会导致代码出现意外的结果,增加了调试和排查错误的难度。

为了避免回调函数地狱,可以采用以下几种方式:

  1. 使用 Promise:Promise 是一种处理异步操作的技术,它可以避免回调函数嵌套,使代码结构更加清晰和易于管理。

  2. 使用 async/await:async/await 是 ES2017 中新增的一种语法,它可以使异步代码看起来像同步代码,避免回调函数嵌套,使代码更加易于理解和维护。

  3. 使用事件发布/订阅模式:事件发布/订阅模式可以解耦异步操作和回调函数,使得代码结构更加清晰和易于管理。

需要注意的是,以上方法都需要浏览器或 Node.js 环境支持,对于一些老旧的浏览器和环境可能不兼容。另外,无论采用什么方式,编写清晰、可读、易于维护的代码都是非常重要的。

Promise 链式调用


连续执行两个或者多个异步操作是一个常见的需求,在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。我们可以通过创造一个 Promise 链来实现这种需求。

见证奇迹的时刻:then() 函数会返回一个和原来不同的新的 Promise

1
2
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);

或者

1
const promise2 = doSomething().then(successCallback, failureCallback);

promise2 不仅表示 doSomething() 函数的完成,也代表了你传入的 successCallback 或者 failureCallback 的完成,这两个函数也可以返回一个 Promise 对象,从而形成另一个异步操作,这样的话,在 promise2 上新增的回调函数会排在这个 Promise 对象的后面。

基本上,每一个 Promise 都代表了链中另一个异步过程的完成。

在不使用链式调用的 情况下,要想做多重的异步操作,会导致经典的 回调地狱

1
2
3
4
5
6
7
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);

现在,可以把 回调绑定到返回的 Promise 上,形成一个 Promise 链

1
2
3
4
5
6
7
8
9
10
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

then 里的参数是可选的,catch(failureCallback)then(null, failureCallback) 的缩略形式。如下所示,我们也可以用箭头函数 来表示:

1
2
3
4
5
6
7
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

注意:一定要有返回值,否则,callback 将无法获取上一个 Promise 的结果。(如果使用箭头函数,() => x() => { return x; } 更简洁一些,但后一种保留 return 的写法才支持使用多个语句。)。

  1. 概念:依靠 then() 方法会返回一个新生成的 Promise 对象特性,继续串联下一环任务,直到结束

  2. 细节:then() 回调函数中的返回值,会影响新生成的 Promise 对象最终状态和结果

  3. 好处:通过链式调用,解决回调函数嵌套问题

    image-20230222173851738

  4. 按照图解,编写核心代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /**
    * 目标:掌握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 对象,串联起来

    image-20230222174946534

  3. 按照图解思路,编写核心代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * 目标:把回调函数嵌套代码,改成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
    })

Promise 链式调用是一种解决回调函数地狱的方案。Promise 链式调用通过在 then() 方法中返回一个新的 Promise 对象,避免了回调函数嵌套的问题,使代码更加易于管理和维护。

使用 Promise 链式调用可以将异步操作和回调函数分离,使得代码更具可读性和可维护性。在 Promise 链式调用中,每个 then() 方法都返回一个 Promise 对象,这个新的 Promise 对象可以用于处理异步操作的结果,或者继续进行链式调用。例如:

1
2
3
4
fetch(url)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));

在这个例子中,fetch() 方法返回一个 Promise 对象,我们可以使用 then() 方法来处理响应结果。第一个 then() 方法将响应结果转换为 JSON 格式,并返回一个新的 Promise 对象。第二个 then() 方法使用 console.log() 方法输出 JSON 数据。如果在链式调用过程中出现错误,可以使用 catch() 方法捕获异常。

在链式调用中,每个 then() 方法返回的 Promise 对象都可以处理异步操作的结果或错误,因此需要在每个 then() 方法中明确地返回一个 Promise 对象。如果在 then() 方法中没有返回 Promise 对象,那么后续的 then() 方法将不会执行。

另外,Promise 链式调用的执行顺序是由 Promise 的状态决定的,如果前一个 Promise 对象的状态为成功,那么后续的 then() 方法将会被执行。如果前一个 Promise 对象的状态为失败,那么后续的 then() 方法将会被跳过,直接执行 catch() 方法。

async 函数 与 await


在 async/await 中,async 函数其实就是一个返回 Promise 对象的生成器函数。async 函数内部使用 await 关键字来暂停执行并等待 Promise 对象的结果,然后将结果作为返回值返回给调用方。调用 async 函数时,可以像调用普通函数一样来处理异步操作的结果,而不需要使用 then() 方法来处理 Promise 对象。

因此,可以说 async/await 是将 Promise 对象和生成器函数结合使用的一种方式,它简化了异步操作的语法,使代码更加易于理解和维护。

async 函数

async 函数是使用async关键字声明的函数。async 函数是 AsyncFunction 构造函数的实例,并且其中允许使用 await 关键字。asyncawait 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 promise

async 函数还可以被作为表达式 来定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function resolveAfter2Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}

async function asyncCall() {
console.log('calling');
const result = await resolveAfter2Seconds();
console.log(result);
// Expected output: "resolved"
}

asyncCall();

> “calling”

>“resolved”

语法
1
2
3
4
5
6
7
8
9
async function name(param0) {
statements
}
async function name(param0, param1) {
statements
}
async function name(param0, param1, /* … ,*/ paramN) {
statements
}
参数
  • name

    函数名称。

  • param 可选

    要传递给函数的参数的名称。

  • statements 可选

    包含函数主体的表达式。可以使用 await 机制。

返回值

一个 Promise,这个 promise 要么会通过一个由 async 函数返回的值被解决,要么会通过一个从 async 函数中抛出的(或其中没有被捕获到的)异常被拒绝。

描述

async 函数可能包含 0 个或者多个 await 表达式。await 表达式会暂停整个 async 函数的执行进程并出让其控制权,只有当其等待的基于 promise 的异步操作被兑现或被拒绝之后才会恢复进程。promise 的解决值会被当作该 await 表达式的返回值。使用 async/await 关键字就可以在异步代码中使用普通的 try/catch 代码块。

备注: await关键字只在 async 函数内有效。如果你在 async 函数体之外使用它,就会抛出语法错误 SyntaxError

备注: async/await的目的为了简化使用基于 promise 的 API 时所需的语法。async/await 的行为就好像搭配使用了生成器和 promise。

async 函数一定会返回一个 promise 对象。如果一个 async 函数的返回值看起来不是 promise,那么它将会被隐式地包装在一个 promise 中。

例如,如下代码:

1
2
3
async function foo() {
return 1;
}

等价于:

1
2
3
function foo() {
return Promise.resolve(1);
}

async 函数的函数体可以被看作是由 0 个或者多个 await 表达式分割开来的。从第一行代码直到(并包括)第一个 await 表达式(如果有的话)都是同步运行的。这样的话,一个不含 await 表达式的 async 函数是会同步运行的。然而,如果函数体内有一个 await 表达式,async 函数就一定会异步执行。

例如:

1
2
3
async function foo() {
await 1;
}

等价于

1
2
3
function foo() {
return Promise.resolve(1).then(() => undefined);
}

这个函数返回一个 Promise 对象,并在 then() 方法中返回 undefined。具体来说,函数 foo() 中使用 Promise.resolve() 方法创建一个已解决的 Promise 对象,然后在 then() 方法中返回 undefined。

由于 then() 方法总是返回一个新的 Promise 对象,因此 foo() 函数返回的是一个 Promise 对象,而不是 undefined。这个 Promise 对象的状态和值如下:

  • 状态:已解决(fulfilled)
  • 值:undefined

返回的 Promise 对象状态为已解决,是因为 then() 方法中没有抛出异常,并且返回了 undefined。由于 Promise 对象的状态不可逆转,因此即使在 then() 方法中抛出异常,Promise 对象的状态也不会改变。

在 then() 方法中返回 undefined 并不是一种常见的操作。通常情况下,我们会在 then() 方法中返回一个值或者一个新的 Promise 对象。如果没有返回值,那么后续的 then() 方法将会收到 undefined。

在 await 表达式之后的代码可以被认为是存在在链式调用的 then 回调中,多个 await 表达式都将加入链式调用的 then 回调中,返回值将作为最后一个 then 回调的返回值。

在接下来的例子中,我们将使用 await 执行两次 promise,整个 foo 函数的执行将会被分为三个阶段。

  1. foo 函数的第一行将会同步执行,await 将会等待 promise 的结束。然后暂停通过 foo 的进程,并将控制权交还给调用 foo 的函数。
  2. 一段时间后,当第一个 promise 完结的时候,控制权将重新回到 foo 函数内。示例中将会将1(promise 状态为 fulfilled)作为结果返回给 await 表达式的左边即 result1。接下来函数会继续进行,到达第二个 await 区域,此时 foo 函数的进程将再次被暂停。
  3. 一段时间后,同样当第二个 promise 完结的时候,result2 将被赋值为 2,之后函数将会正常同步执行,将默认返回undefined
1
2
3
4
5
6
7
8
9
async function foo() {
const result1 = await new Promise((resolve) =>
setTimeout(() => resolve("1"))
);
const result2 = await new Promise((resolve) =>
setTimeout(() => resolve("2"))
);
}
foo();

注意:promise 链不是一次就构建好的,相反,promise 链是分阶段构造的,因此在处理异步函数时必须注意对错误函数的处理。

例如,在下面代码中,即使在 promise 链中进一步配置了 .catch 方法处理,也会抛出一个未处理的 promise 被拒绝的错误。这是因为 p2 直到控制从 p1 返回后才会连接到 promise 链。

1
2
3
4
5
6
async function foo() {
const p1 = new Promise((resolve) => setTimeout(() => resolve("1"), 1000));
const p2 = new Promise((_, reject) => setTimeout(() => reject("2"), 500));
const results = [await p1, await p2]; // 不推荐使用这种方式,请使用 Promise.all 或者 Promise.allSettled
}
foo().catch(() => {}); // 捕捉所有的错误...

await 和并行

在 JavaScript 中,使用 await 关键字可以等待一个异步操作的结果,然后继续执行后续的代码。使用 await 关键字可以使异步操作看起来像同步操作,使代码结构更加清晰和易于理解。

虽然使用 await 关键字可以等待异步操作的结果,但是它并不能实现并行操作。使用 await 关键字时,代码会等待异步操作完成后才会继续执行后续的代码,因此无法同时执行多个异步操作。

如果需要同时执行多个异步操作,可以使用 Promise.all() 方法。 Promise.all() 方法接受一个 Promise 对象数组作为参数,并返回一个新的 Promise 对象。新的 Promise 对象在所有输入的 Promise 对象都已成功解决时解决,并且返回的值是一个包含所有 Promise 对象的解决值的数组。

例如,下面的代码使用 Promise.all() 方法实现了并行执行两个异步操作:

1
2
3
4
5
6
7
async function fetchData() {
const [result1, result2] = await Promise.all([
fetch('/api/data1'),
fetch('/api/data2')
]);
console.log(result1, result2);
}

在上面的代码中,使用 await Promise.all() 方法等待两个异步操作完成,并将结果解构为 result1result2 两个变量。由于 Promise.all() 方法会在所有输入的 Promise 对象都已成功解决时才返回,因此可以保证 result1result2 变量都已经赋值。然后,我们可以使用这两个变量来处理异步操作的结果。

需要注意的是,使用 Promise.all() 方法并不总是能够提高程序的性能,因为它会同时执行多个异步操作,可能会导致服务器负载过高。因此,在实际使用中,需要根据具体情况来选择合适的方案。

如果希望并行执行两个或更多的任务,你必须像在parallel中一样使用await Promise.all([job1(), job2()])

async/await 和 Promise/then 对比以及错误处理

大多数 async 函数也可以使用 Promises 编写。但是,在错误处理方面,async 函数更容易捕获异常错误

  1. 对比:

    Promise/then:

    1
    2
    3
    4
    5
    6
    function fetchData() {
    fetch('/api/data')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error(error));
    }

    async/await:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    async function fetchData() {
    try {
    const response = await fetch('/api/data');
    const data = await response.json();
    console.log(data);
    } catch (error) {
    console.error(error);
    }
    }

    Promise/then 的优点是可以实现链式调用,使得代码结构清晰,易于理解和维护。它的缺点是需要嵌套多层 then() 方法,代码嵌套深度过深时会出现回调函数地狱的问题。

    async/await 的优点是可以使异步代码看起来像同步代码,避免了回调函数地狱的问题,使代码结构更加清晰和易于管理。它的缺点是不能像 Promise/then 那样实现链式调用,每个异步操作都需要使用 await 关键字等待结果。

  2. 错误处理:

    在 Promise/then 中,可以使用 catch() 方法来处理异常。catch() 方法会捕获前面的 Promise 对象抛出的错误,并执行指定的回调函数。

    在 async/await 中,可以使用 try-catch 语句来处理异常。在 try 语句中使用 await 关键字等待异步操作的结果,如果出现异常,会跳转到 catch 语句中执行异常处理代码。

在 Promise/then 中,如果没有使用 catch() 方法来处理异常,异常将会被抛出到全局作用域中。在 async/await 中,如果没有使用 try-catch 语句来处理异常,异常也会被抛出到全局作用域中。

但是,async 函数仍有可能错误地忽略错误。以 parallel async 函数为例。如果它没有等待 await(或返回)Promise.all([]) 调用的结果,则不会传播任何错误。虽然 parallelPromise 函数示例看起来很简单,但它根本不会处理错误!这样做需要一个类似于 return Promise.all([]) 处理方式。

以下是一个修改后的 parallel async 函数示例,它使用 try-catch 语句来捕获可能抛出的异常,并将错误传播给调用方:

1
2
3
4
5
6
7
async function parallel(tasks) {
try {
return await Promise.all(tasks.map(task => task()));
} catch (error) {
throw error;
}
}

在这个示例中,我们使用 try-catch 语句来捕获 Promise.all() 调用中可能抛出的异常,并在遇到异常时使用 throw 关键字将错误传播给调用方。这样可以确保在遇到异常时及时处理,并将错误传播到正确的位置

即使使用 async/await,也需要正确地处理错误。在使用 async/await 时,需要确保使用 try-catch 语句来捕获异步操作可能抛出的异常,并且在遇到异常时及时处理,以避免错误被忽略。

在 parallel async 函数中,如果没有等待 Promise.all() 调用的结果,并且其中一个异步操作抛出了异常,那么这个异常将会被忽略,并且不会被传播到调用方。因此,需要确保捕获并处理 Promise.all() 调用中所有 Promise 对象可能抛出的异常,以确保错误能够被正确地处理和传播。

使用 async 函数重写 promise 链

返回 Promise的 API 将会产生一个 promise 链,它将函数肢解成许多部分。例如下面的代码:

1
2
3
4
5
6
7
8
9
function getProcessedData(url) {
return downloadData(url) // 返回一个 promise 对象
.catch(e => {
return downloadFallbackData(url) // 返回一个 promise 对象
})
.then(v => {
return processDataInWorker(v); // 返回一个 promise 对象
});
}

可以重写为单个 async 函数:

1
2
3
4
5
6
7
8
9
async function getProcessedData(url) {
let v;
try {
v = await downloadData(url);
} catch (e) {
v = await downloadFallbackData(url);
}
return processDataInWorker(v);
}

注意,在上述示例中,return 语句中没有 await 运算符,因为 async function 的返回值将被隐式地传递给 Promise.resolve

如果 return 语句后面跟着一个 Promise 对象,或者一个使用 await 关键字等待的异步操作,那么这个 Promise 对象或异步操作的结果将会成为函数的返回值。如果 return 语句后面跟着一个普通的值,那么这个值将会被包装在一个已解决的 Promise 对象中,并成为函数的返回值。

返回值隐式地传递给 Promise.resolve,并不意味着 return await promiseValue;return promiseValue; 在功能上相同。

看下下面重写的上面代码,在 processDataInWorker 抛出异常时返回了 null:

1
2
3
4
5
6
7
8
9
10
11
12
13
async function getProcessedData(url) {
let v;
try {
v = await downloadData(url);
} catch(e) {
v = await downloadFallbackData(url);
}
try {
return await processDataInWorker(v); // 注意 `return await` 和单独 `return` 的比较
} catch (e) {
return null;
}
}

简单地写上return processDataInworker(v); 将导致在 processDataInWorker(v) 出错时 function 返回值为Promise而不是返回 nullreturn foo;return await foo;,有一些细微的差异:return foo;不管foo是 promise 还是 rejects 都将会直接返回foo。相反地,如果foo是一个Promisereturn await foo;将等待foo执行 (resolve) 或拒绝 (reject),如果是拒绝,将会在返回前抛出异常。

async 函数和 await 捕获错误

async/await 中,可以使用 try-catch 语句来捕获异步操作可能抛出的异常。

await 操作符等待一个 Promise 对象时,如果这个 Promise 对象被拒绝,那么 await 操作符会抛出一个异常。如果没有使用 try-catch 语句来捕获这个异常,那么异常将会被传递给调用方,可能会导致程序崩溃或产生不可预料的结果。

以下是一个示例代码,它展示了在 async/await 中如何捕获异常:

1
2
3
4
5
6
7
8
9
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}

在上面的代码中,我们使用 try-catch 语句来捕获 fetch()response.json() 方法可能抛出的异常,并在遇到异常时使用 console.error() 方法输出错误信息。这样可以确保程序能够正确地处理异常,并避免程序崩溃或产生不可预料的结果。

需要注意的是,在使用 async/await 时,需要使用 try-catch 语句来捕获可能抛出的异常,并进行错误处理。

事件循环 -Event Loop


之所以称之为 事件循环,是因为它经常按照类似如下的方式来被实现:

1
2
3
while (queue.waitForMessage()) {
queue.processNextMessage();
}

queue.waitForMessage() 会同步地等待消息到达 (如果当前没有任何消息等待被处理)。

事件循环(Event Loop)是 JavaScript 中一种处理异步操作的机制。它是一种在单线程环境下处理并发操作的方式,能够确保 JavaScript 中的异步操作能够按照预期的顺序执行,并且不会阻塞主线程。

事件循环的工作方式如下:

  1. 执行同步任务:JavaScript 引擎会首先执行当前执行栈中的同步任务,直到执行栈为空。
  2. 处理微任务队列:在执行完同步任务后,JavaScript 引擎会检查是否存在已经完成的微任务,如果存在,那么会依次执行所有的微任务,直到微任务队列为空。
  3. 处理宏任务队列:在执行完微任务后,JavaScript 引擎会检查是否存在已经完成的宏任务,如果存在,那么会从宏任务队列中取出一个任务并执行,然后再次回到第 2 步处理微任务队列。如果宏任务队列为空,则会等待新的任务被添加到队列中。
  4. 微任务和宏任务的执行顺序不同。微任务通常是在当前任务完成后立即执行,而宏任务通常是在下一个事件循环周期中执行。因此,微任务的执行优先级高于宏任务。

以下是一个示例代码,它展示了事件循环的工作方式:

1
2
3
4
5
6
7
8
9
10
11
console.log('1');

setTimeout(() => {
console.log('2');
}, 0);

Promise.resolve().then(() => {
console.log('3');
});

console.log('4');

在上面的代码中,我们首先输出字符串 '1''4'。然后,我们使用 setTimeout() 方法注册一个宏任务,并在 0 毫秒后执行它。接着,我们使用 Promise.resolve().then() 方法注册一个微任务,并在微任务队列中排队等待执行。最后,我们输出字符串 '3''2'

由于微任务的执行优先级高于宏任务,因此在执行完同步任务后,JavaScript 引擎会优先执行微任务,输出字符串 '3'。然后,JavaScript 引擎会从宏任务队列中取出一个任务,并在下一个事件循环周期中执行它,输出字符串 '2'

需要注意的是,在使用异步操作时,需要确保理解事件循环的工作方式,并避免出现意外的结果。例如,在使用 setTimeout() 方法时,需要注意宏任务的执行时间,并避免出现因为宏任务执行时间过长而阻塞主线程的情况。

执行至完成

每一个消息完整地执行后,其他消息才会被执行。这为程序的分析提供了一些优秀的特性,包括:当一个函数执行时,它不会被抢占,只有在它运行完毕之后才会去运行任何其他的代码,才能修改这个函数操作的数据。这与 C 语言不同,例如,如果函数在线程中运行,它可能在任何位置被终止,然后在另一个线程中运行其他代码。

这个模型的一个缺点在于当一个消息需要太长时间才能处理完毕时,Web 应用程序就无法处理与用户的交互,例如点击或滚动。为了缓解这个问题,浏览器一般会弹出一个“这个脚本运行时间过长”的对话框。一个良好的习惯是缩短单个消息处理时间,并在可能的情况下将一个消息裁剪成多个消息。

添加信息

在浏览器里,每当一个事件发生并且有一个事件监听器绑定在该事件上时,一个消息就会被添加进消息队列。如果没有事件监听器,这个事件将会丢失。所以当一个带有点击事件处理器的元素被点击时,就会像其他事件一样产生一个类似的消息。

函数 setTimeout 接受两个参数:待加入队列的消息和一个时间值(可选,默认为 0)。这个时间值代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其他消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。但是,如果有其他消息,setTimeout 消息必须等待其他消息处理完。因此第二个参数仅仅表示最少延迟时间,而非确切的等待时间。

下面的例子演示了这个概念(setTimeout 并不会在计时器到期之后直接执行):

1
2
3
4
5
6
7
8
9
10
11
12
13
const s = new Date().getSeconds();

setTimeout(function() {
// 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);

while(true) {
if(new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}

零延迟

零延迟并不意味着回调会立即执行。以 0 为第二参数调用 setTimeout 并不表示在 0 毫秒后就立即调用回调函数。

其等待的时间取决于队列里待处理的消息数量。在下面的例子中,"这是一条消息" 将会在回调获得处理之前输出到控制台,这是因为延迟参数是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间。

基本上,setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function() {

console.log('这是开始');

setTimeout(function cb() {
console.log('这是来自第一个回调的消息');
});

console.log('这是一条消息');

setTimeout(function cb1() {
console.log('这是来自第二个回调的消息');
}, 0);

console.log('这是结束');

})();

// "这是开始"
// "这是一条消息"
// "这是结束"
// "这是来自第一个回调的消息"
// "这是来自第二个回调的消息"

多个运行时互相通信

一个 web worker 或者一个跨域的 iframe 都有自己的栈、堆和消息队列。两个不同的运行时只能通过 postMessage 方法进行通信。如果另一个运行时侦听 message 事件,则此方法会向该运行时添加消息。

永不阻塞

JavaScript 的事件循环模型与许多其他语言不同的一个非常有趣的特性是,它永不阻塞。处理 I/O 通常通过事件和回调来执行,所以当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR 请求返回时,它仍然可以处理其他事情,比如用户输入。

由于历史原因有一些例外,如 alert 或者同步 XHR,但应该尽量避免使用它们

宏任务与微任务


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

  2. 异步任务划分为了

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

    image-20230222184920343

  4. 事件循环模型

    具体运行效果,参考 PPT 动画或者视频

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * 目标:阅读并回答打印的执行顺序
    */
    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)

    image-20230222184949605

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

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

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

  • 宏任务:浏览器执行的异步代码
    • 例如:JS 执行脚本事件,setTimeout/setInterval,AJAX请求完成事件,用户交互事件等
  • 微任务:JS 引擎执行的异步代码
    • 例如:Promise对象.then()的回调
  • JavaScript 内代码如何执行?
    • 执行第一个 script 脚本事件宏任务,里面同步代码
    • 遇到 宏任务/微任务 交给宿主环境,有结果回调函数进入对应队列
    • 当执行栈空闲时,清空微任务队列,再执行下一个宏任务,从1再来

Promise.all() 静态方法


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

    image-20230222190117045
  2. 语法:

    1
    2
    3
    4
    5
    6
    const p = Promise.all([Promise对象, Promise对象, ...])
    p.then(result => {
    // result 结果: [Promise对象成功结果, Promise对象成功结果, ...]
    }).catch(error => {
    // 第一个失败的 Promise 对象,抛出的异常对象
    })
  3. 需求:同时请求“北京”,“上海”,“广州”,“深圳”的天气并在网页尽可能同时显示

    image-20230222190230351
  4. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    <!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>
  • Promise.all() 什么时候用 : 合并多个 Promise 对象并等待所有同时成功的结果,如果有一个报错就会最终为失败状态,当需要同时渲染多个接口数据同时到网页上时使用

Promise.all() 是一个静态方法,它可以将多个 Promise 对象包装成一个新的 Promise 对象,并在所有的 Promise 对象都完成后,返回一个包含所有 Promise 结果的数组。

Promise.all() 方法的使用方式如下:

1
2
3
4
5
6
7
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log(results);
})
.catch(error => {
console.error(error);
});

在上面的示例代码中,我们使用 Promise.all() 方法将三个 Promise 对象包装成一个新的 Promise 对象。然后,我们使用 then() 方法注册一个回调函数,当所有的 Promise 对象都完成后,会将它们的结果以数组的形式传递给这个回调函数。如果在执行 Promise.all() 方法时,任何一个 Promise 对象被拒绝,那么会立即跳转到 catch() 方法中,并输出错误信息。

需要注意的是,如果传递给 Promise.all() 方法的数组中有一个或多个元素不是 Promise 对象,那么它们会被自动包装在一个已解决的 Promise 对象中。如果数组中的所有元素都不是 Promise 对象,那么 Promise.all() 方法会立即返回一个已解决的 Promise 对象,并包含原始数组作为它的结果。

以下是一个示例代码,它展示了 Promise.all() 方法的使用方式:

1
2
3
4
5
6
7
8
9
10
11
const promise1 = Promise.resolve(1);
const promise2 = new Promise(resolve => setTimeout(() => resolve(2), 1000));
const promise3 = fetch('/api/data').then(response => response.json());

Promise.all([promise1, promise2, promise3])
.then(results => {
console.log(results);
})
.catch(error => {
console.error(error);
});

在上面的代码中,我们创建了三个 Promise 对象,它们分别返回值 1,在 1 秒钟后返回值 2,以及使用 fetch() 方法异步获取数据。然后,我们使用 Promise.all() 方法将这三个 Promise 对象包装成一个新的 Promise 对象,并在所有的 Promise 对象都完成后,输出它们的结果。

由于在这个示例代码中,我们使用了 fetch() 方法异步获取数据,因此需要在执行 Promise.all() 方法之前等待这个异步操作完成。否则,结果数组中可能会缺少这个异步操作的结果。

案例


案例一: 商品分类

  1. 需求:尽可能同时展示所有商品分类到页面上

    image-20230222191151264

  2. 步骤:

    1. 获取所有的一级分类数据

    2. 遍历id,创建获取二级分类请求

    3. 合并所有二级分类Promise对象

    4. 等待同时成功,开始渲染页面

  3. 核心代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    /**
    * 目标:把所有商品分类“同时”渲染到页面上
    * 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
    })
    })

案例二:省市区切换

  1. 需求:完成省市区切换效果

    image-20230222191239971

  2. 步骤:

    1. 设置省份数据到下拉菜单

    2. 切换省份,设置城市数据到下拉菜单,并清空地区下拉菜单

    3. 切换城市,设置地区数据到下拉菜单

  3. 核心代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    /**
    * 目标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
    })

案例三:数据提交

  1. 需求:收集学习反馈数据,提交保存

    image-20230222191239971

  2. 步骤:

    1. 监听提交按钮的点击事件

    2. 依靠插件收集表单数据

    3. 基于 axios 提交保存,显示结果

  3. 核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /**
    * 目标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)
    }
    })
  • Title: Ajax的原理与进阶
  • Author: cccs7
  • Created at: 2023-07-03 13:23:00
  • Updated at: 2023-07-06 18:35:52
  • Link: https://blog.cccs7.icu/2023/07/03/Ajax/
  • License: This work is licensed under CC BY-NC-SA 4.0.
 Comments