五 异步编程
5.5 回调地狱
回调地狱这个词听起来就非常的高大上,在接触Promise之前,必须要懂得什么是回调地狱,以及为什么会产生回调地狱?
先来看看概念:当一个回调函数嵌套一个回调函数的时候就会出现一个嵌套结构,当嵌套的多了就会出现回调地狱的情况。
根据前面我们可以得出一个结论:存在异步任务的代码,不能保证能按照顺序执行,那如果我们非要代码顺序执行呢?
举个例子:让四个回调函数打印骆宾王的《咏鹅》,咱们必须要这样操作,才能保证顺序正确:
javascript
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {
console.log("红掌拨清波")
}, 1000)
console.log("白毛浮绿水")
}, 2000)
console.log("曲项向天歌")
}, 3000)
console.log("鹅鹅鹅")
}, 4000)
可以看到,代码中的回调函数套回调函数,居然套了4层,这种回调函数中嵌套回调函数的情况 就叫做回调地狱。
总结一下,回调地狱就是为实现代码顺序执行而出现的一种操作,它会造成我们的代码可读性非常差,后期不好维护。
那该如何解决回调地狱呢?
两种方式
使用ES6引入的promise
async/await
5.6 promise的应用
Promise 是js中的一个原生对象(也可以理解为一个类或者一个构造函数),是一种异步编程的解决方案,可以替换掉传统的回调函数解决方案。
Promise 是一种**对异步操作的封装,**表示一个操作现在可能还没有完成,但将来会完成,并提供了成功或失败时要执行的操作。
Promise: ES6引入的一个新的概念(可以理解为是一个类,或者是一个构造函数)
引入的目的主要是解决回调地狱的写法
异步编程想要做到类似于同步变成的顺序执行,才会涉及到回调地狱
5.6.1 Promise的三种状态
Promise(承诺) 的状态: 回调函数执行:从开始到结束。
pending : 等待状态: 承诺的这件事没有结果(结果只有两种情况,要么成功,要么失败),也可以理解为对象刚刚初始化完毕时的状态
fulfilled : 完成,成功状态: 承诺的这件事得到满意的结果。
rejected: 失败,拒绝状态: 承诺的这件事得到不满意的结果。比如类似ajax发送请求,结果收到了404,500这类的状态码
5.6.2 Promise的参数
Promise构造函数的参数,需要我们传入一个函数,我们需要处理的异步任务就是书写在该函数体内。传入的这个函数,要求必须要有两个参数,第一个参数是异步任务执行成功时的回调函数,第二个参数是异步任务执行失败时的回调函数 ,正常来说,参数名叫什么都无所谓,但是咱们尽量做到见名知意 嘛,所以规范中命名为resolve(解决的含义),reject(拒绝) 。异步任务执行成功时调用resolve函数返回结果,反之调用reject。
Promise构造函数的参数:
1.参数是一个回调函数 。(参数1,参数2)=>{......}
2.回调函数里的第一个参数是一个函数,表示成功状态调用的函数 ,因此参数名可以做到见名知意,叫resolve(解决的含义)
3.回调函数里的第二个参数也是一个函数,表示失败、拒绝状态调用的函数 ,因此参数名可以做到见名知意,叫reject(拒绝的含义)
javascript
let p = new Promise( (resolve,reject)=>{
// 使用伪代码来模拟开启的一部编程的任务是否成功执行,或者是失败执行
/*
随机数为0~70: 表示任务执行成功
随机数为71~100: 表示任务执行失败
*/
let num1 = Math.round(Math.random()*100);
console.log(num1);
if (num1<=70){
resolve("任务1执行成功");
}else{
reject("任务1执行失败了");
}
});
5.6.3 常用API
- then()
Promise对象的then方法用来接收处理成功时响应的数据。
then(onfulfilled,onrejected): Promise对象调用该方法
- 第一个参数onfulfilled : 用于给Promise的回调函数(参数)的第一个参数resolve赋值,赋的值是一个函数 。
函数的样子: (data)=>{......} :任务执行的结果作为参数传递给data
- 第二个参数onrejected : 用于给Promise的回调函数的第二个参数reject赋值,赋的值也是一个函数。
函数的样子: (reason)=>{......} :任务执行失败的结果作为参数传递给reason
作用:任务失败时,执行逻辑的封装。 可以代替catch。
应用场景:每个人物的失败都像单独处理,可以用第二个参数。
- catch()
catch方法用来接收处理失败时相应的数据。
catch(onrejected)方法: 任务失败时执行该方法,Promise对象调用该方法1.参数只有一个onrejected:也是一个函数 ,用于给Promise的回调函数(参数)的第二个参数reject赋值,赋的值也是一个函数。
函数的样子:(reason)=>{......}
作用:所有的任务在失败时想要统一处理时,使用catch。
Promise的链式调用:
p.then().then().then()...
注意:then()方法是Promise对象的,因此,想要链式调用then方法,那么前一个then必须返回一个新的Promise对象
演示代码:
javascript
let p = new Promise( (resolve,reject)=>{
// 使用伪代码来模拟开启的一部编程的任务是否成功执行,或者是失败执行
/*
随机数为0~70: 表示任务执行成功
随机数为71~100: 表示任务执行失败
*/
let num1 = Math.round(Math.random()*100);
console.log(num1);
if (num1<=70){
resolve("任务1执行成功");
}else{
reject("任务1执行失败了");
}
});
// let f1 = (data)=>{
// console.log("onfulfilled: "+data);
// }
// let f2 = (data)=>{
// console.log("onrejected: "+data);
// }
// p.then(f1,f2);
p.then((data)=>{
console.log("onfulfilled: "+data);
//第二个任务: 必须在任务1成功执行后,执行
return new Promise((resolve,reject)=>{
let num2 = Math.round(Math.random()*100)+100;
console.log(num2);
if (num2<=170){
resolve("任务2执行成功");
}else{
reject("任务2执行失败了");
}
})
},(reason)=>{
console.log("失败原因1: "+reason);
return new Promise((resolve,reject)=>{
let num2 = Math.round(Math.random()*100)+100;
console.log(num2);
if (num2<=170){
resolve("任务2执行成功");
}else{
reject("任务2执行失败了");
}
})
}).then((data)=>{
console.log("第二个任务: "+data);
},(reason)=>{
console.log("失败原因2: "+reason);
})
// .catch((reason)=>{
// console.log("失败原因: "+reason);
// })
// .catch((reason)=>{
// console.log("onrejected: "+reason);
// })
Promise完成异步的顺序执行代码
javascript
//
// let p =new Promise((resolve,reject)=>{
// resolve("鹅鹅鹅");
// })
//
// p.then((value)=>{
// console.log(value);
// return new Promise((resolve,reject)=>{
// resolve("曲项向天歌");
// })
// }).then((value)=>{
// console.log(value);
// return new Promise((resolve,reject)=>{
// resolve("白毛浮绿水");
// })
// }).then((value)=>{
// console.log(value);
// return new Promise((resolve,reject)=>{
// resolve("红掌拨清波");
// })
// }).then((value)=>{
// console.log(value);
// })
let p1=new Promise((resolve,reject)=>{
let num =Math.round(Math.random()*50);
if (num<=30){
resolve("鹅鹅鹅");
}else{
reject("锄禾日当午");
}
})
p1.then((data)=>{
console.log(data)
return new Promise((resolve,reject)=>{
let num2 =Math.round(Math.random()*50);
if (num2<=30){
resolve("曲项向天歌");
}else{
reject("汗滴禾下土");
}
})
},(value)=>{
console.log(value);
return new Promise((resolve,reject)=>{
let num2 =Math.round(Math.random()*50);
if (num2<=30){
resolve("曲项向天歌");
}else{
reject("汗滴禾下土");
}
})
}).then((data)=>{
console.log(data);
return new Promise((resolve,reject)=>{
let num2 =Math.round(Math.random()*50);
if (num2<=30){
resolve("白毛浮绿水");
}else{
reject("谁知盘中餐");
}
})
},(value)=>{
console.log(value);
return new Promise((resolve,reject)=>{
let num2 =Math.round(Math.random()*50);
if (num2<=30){
resolve("白毛浮绿水");
}else{
reject("谁知盘中餐");
}
})
}).then((data)=>{
console.log(data);
return new Promise((resolve,reject)=>{
let num2 =Math.round(Math.random()*50);
if (num2<=30){
resolve("红掌拨清波");
}else{
reject("粒粒皆辛苦");
}
})
},(value)=>{
console.log(value);
return new Promise((resolve,reject)=>{
let num2 =Math.round(Math.random()*50);
if (num2<=30){
resolve("红掌拨清波");
}else{
reject("粒粒皆辛苦");
}
})
}).then((data)=>{
console.log(data);
},(value)=>{
console.log(value);
})
5.7 Fetch
Fetch: 是一个基于Promise的API,可以发送HTTP请求
第一种:发送get请求
fetch(url).then((response)=>{...})
解析:
1.url:请求路径,get请求需要将请求参数拼接到url上。
2.第一个then里的匿名函数是用于处理请求成功后服务器响应的数据,形参resopnse是整个想要对象的封装,里面含有很多键值对的信息
比如: type,url,status,redirected,ok, 已经服务端绑定的数据。
3.如果想要获取服务端发送过来的数据,
需要 return 响应对象上的json() 实际上返回的是一个新的Promise
4.因此,如果想要继续处理数据,那么需要继续调用then方法。
- 如果处理的是异常信息,可以使用catch
演示代码:
javascript
let btn1 = document.querySelector("#btn1");
// alert(btn1.nodeName);
btn1.addEventListener("click", function (){
fetch("checkLogin.lr?username=zhangsan").then((data)=>{
console.log(data);
return data.json();
}).then((data)=>{
console.log(data);
// console.log(data.error_msg);
document.querySelector("#s1").innerText = data.error_msg;
})
})
第二种:发送post请求
1.格式: fetch(url,{method:"post",body:"请求参数"})
2.解析: url: 请求路径
{}用于指定其他的参数
method:请求方式,默认为get
body:用于指定请求参数,使用对象类型的写法。还需要使用URLSearchParams类型进行包装
如果method制定了get请求,那么就不需要body属性,而是将请求参数拼接到url上
演示代码
javascript
let btn2 = document.querySelector("#btn2");
// alert(btn1.nodeName);
btn2.addEventListener("click", function (){
fetch("checkLogin.lr",
{method:"post",
headers:{'content-type':'application/x-www-form-urlencoded'},
body:"username=zhangsan"
}).then((data)=>{
console.log(data)
return data.json();
}).then((jsonStr)=>{
console.log(jsonStr.error_msg);
})
})
第三种: 发送post请求,并且发送json格式的数据
演示代码
javascript
let btn3 = document.querySelector("#btn3");
let obj = {username:"lisi"};
// alert(btn1.nodeName);
btn3.addEventListener("click", function (){
fetch("checkLogin.lr", {
method:"post",
// headers:{'content-type':'application/x-www-form-urlencoded'},
body:JSON.stringify(obj)
}).then((data)=>{
console.log(data)
return data.json();
}).then((jsonStr)=>{
console.log(jsonStr.error_msg);
})
})
5.8 Async/Await
Async/await 是建立在Promises之上 的语法糖 ,使得异步代码的编写和理解更接近传统的同步代码风格 。(使用同步的方式编写异步代码)
5.8.1 Async关键字
声明函数时,在前面添加Async关键字,表示该函数为一个异步任务,不会阻塞后面函数的执行。
javascript
async function fn(){
return '任务1';
}
console.log(fn()); // Promise{fulfilled: "任务1"}
打印可以看到async函数返回数据时自动封装为一个Promise对象。
和Promise对象一样,处理异步任务时也可以按照成功和失败来返回不同的数据,处理成功时用then方法来接收,失败时用catch方法来接收数据:
虽然Promise可以解决回调地狱的问题,但是Promise变成充斥着大量的then函数。
因此可以使用Async/Await来简化Promise的异步编程。
Async/Await是建立在Promise之上的一个语法糖,可以使用同步代码的风格实现异步编程。
Async: 异步的意思, 用法简单,只需要在定义的函数前,添加async关键字即可。
async声明的函数,会隐式返回一个Promise对象 。
async声明的函数,还是使用then或者catch来调用
函数中如果正确使用return关键字进行了数据的返回,那么Promise的状态就是fulfilled。
如果函数中的代码抛出了异常,那么Promise的状态就是rejected。4.async修改的函数,本质上是Promise,调用时还是异步代码,并不会阻塞后续的代码运行。
5.在普通函数前添加async关键字,等价于在普通函数中return 一个Promise对象
javascript
async function f1(){
// console.log("任务一");
try{
return "任务一";
}catch (e){
throw new Error("任务一出错了")
}
}
// function f1(){
// return new Promise((resolve,reject)=>{
// try{
// //......省略大量代码
// resolve("任务一")
// }catch (e){
// reject("任务一出错了")
// }finally {
// //释放资源
// }
//
// })
// }
f1().then((value)=>{
console.log(value);
},(reason)=>{
console.log(reason);
})
console.log("---我是主线程代码---")
5.8.2 Await关键字
await关键字只能在使用async定义的函数中使用
await后面可以直接跟一个 Promise实例对象(可以跟任何表达式,更多的是跟一个返回Promise对象的表达式)
await函数不能单独使用, await等待的意思,可以等待任何表达式,不过最常用的是等待一个Promise对象。
await可以直接拿到Promise中resolve中的数据。
await后续的代码什么时候才执行? 等到结果后,才会执行。 也就是await会阻塞后面的代码
测试代码:
javascript
// 两秒后获得一个随机数
function getRandomNumber(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(Math.round(Math.random()*3));
},2000)
})
}
// async function getRandomNumber(){
// setTimeout(()=>{
// return Math.round(Math.random()*3);
// },2000)
// }
async function test(){
// 2秒后,获取一个随机数
let r1 = await getRandomNumber(); //等2秒,可以获取随机数,给变量r1
let r2 = await getRandomNumber(); //继续等2秒,获取第二个随机数,给变量r2
let r3 = await getRandomNumber(); // 继续等2秒,获取第三个随机数,给变量r3
console.log(r1,r2,r3);
}
test();
console.log("---我是6秒前执行的,await关键字并不影响主线程中的代码执行。---")
5.9 Axios
5.9.1 Axios简介
Axios(中文谐音:爱克丝赛欧斯)
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
源码:https://gitee.com/charlinchenlin/store-pos
特性:
1、从浏览器中创建 XMLHttpRequests
2、从 node.js 创建 http 请求
3、支持 Promise API
4、拦截请求和响应
5、转换请求数据和响应数据
6、取消请求
7、自动转换 JSON 数据
8、客户端支持防御 XSRF
5.9.2 安装方式
使用 npm:
$ npm install axios
使用 bower:
$ bower install axios
使用CDN
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
5.9.3 案例演示
javascript
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>axios</title>
</head>
<body>
<button id="btn1">axios发生get请求--方法</button>
<button id="btn2">axios发生post请求--方法</button>
<button id="btn3">axios发生get请求</button>
<button id="btn4">axios发生post请求</button>
<br><span id="s1" style="color: red"></span>
</body>
<script src="js/axios.js"></script>
<script>
let btn1 = document.querySelector("#btn1");
let btn2 = document.querySelector("#btn2");
let btn3 = document.querySelector("#btn3");
let btn4 = document.querySelector("#btn4");
/*
axios.get((url,{}))
解析: url: 请求资源路径
{}:用于指定其他参数的,比如请求参数,注意请求参数的key是固定写法,必须叫params
params的值,也只能是对象形式。
*/
btn1.addEventListener("click",()=>{
//模拟从文本框拿到数据后做了一个对象的封装
let loginInfo = {"username":'zhangsan',password:123456}
axios.get("checkLogin.lr",{params:loginInfo}).then((response)=>{
// 因为response这个对象上有一个叫data的属性,该属性用于存储服务端发送过来的数据,因此直接使用data属性即可
// console.log(response.data)
let obj = response.data;
document.querySelector("#s1").innerText=obj.error_msg;
})
})
/**
* 后端Servlet只能从getReader()里读取数据,getParameter(...)不行
*/
// btn2.addEventListener("click",()=>{
// //模拟从文本框拿到数据后做了一个对象的封装
// let loginInfo = {"username":'zhangsan',password:123456};
// axios.post("checkLogin.lr",loginInfo).then((response)=>{
// // 因为response这个对象上有一个叫data的属性,该属性用于存储服务端发送过来的数据,因此直接使用data属性即可
// console.log(response.data)
// let obj = response.data;
// document.querySelector("#s1").innerText=obj.error_msg;
// })
// })
btn2.addEventListener("click",()=>{
//模拟从文本框拿到数据后做了一个对象的封装
let loginInfo = {"username":'zhangsan',password:123456};
axios.post("checkLogin.lr","username=zhangsan&&password=123456").then((response)=>{
// 因为response这个对象上有一个叫data的属性,该属性用于存储服务端发送过来的数据,因此直接使用data属性即可
console.log(response.data)
let obj = response.data;
document.querySelector("#s1").innerText=obj.error_msg;
})
})
/**
* axios({
* url: 请求路径,
* method: 请求方式,默认为get,
* params/data: get请求时,使用params传请求参数,post请求时,必须使用data传请求参数
* headers: 可选,post请求时,必须设置
* })
*/
btn3.addEventListener("click",()=>{
//模拟从文本框拿到数据后做了一个对象的封装
let loginInfo = {"username":'zhangsan',password:123456};
axios({
url:"checkLogin.lr",
method:"get",
params:loginInfo
}).then((response)=>{
// 因为response这个对象上有一个叫data的属性,该属性用于存储服务端发送过来的数据,因此直接使用data属性即可
console.log(response.data)
let obj = response.data;
document.querySelector("#s1").innerText=obj.error_msg;
})
})
btn4.addEventListener("click",()=>{
//模拟从文本框拿到数据后做了一个对象的封装
let loginInfo = {"username":'zhangsan',password:123456};
axios({
url:"checkLogin.lr",
method:"post",
headers:{"content-type":"application/x-www-form-urlencoded"},
data:loginInfo
}).then((response)=>{
// 因为response这个对象上有一个叫data的属性,该属性用于存储服务端发送过来的数据,因此直接使用data属性即可
console.log(response.data)
let obj = response.data;
document.querySelector("#s1").innerText=obj.error_msg;
}).catch((error)=>{
console.log(error);
})
})
</script>
</html>