一、回调地狱
首先了解两个概念,什么是回调函数?什么是异步任务?
1.1回调函数
当一个函数作为参数传入另一个参数中,并且它不会立即执行,只有当满足一定条件后该函数才可以执行,这种函数就称为回调函数。
(它是作为参数传递给另一个函数的函数)
我们熟悉的定时器和Ajax中就存在有回调函数:
定时器setTimeout:
setTimeout(function(){
//function(){console.log('执行了回调函数')}就是回调函数,
//它只有在2秒后才会执行
console.log('2秒后执行了回调函数');
},2000) //2000毫秒
Ajax:
//1.创建异步对象
var xhr=new XMLHttpRequest();
//2.绑定监听事件(接收请求)
xhr.onreadystatechange=function(){
//此方法会被调用4次
//最后一次,readyState==4
//并且响应状态码为200时,才是我们要的响应结果 xhr.status==200
if(xhr.readyState==4 && xhr.status==200){
//把响应数据存储到变量result中
var result=xhr.responseText;
console.log(result);
}
}
//3.打开链接(创建请求)
xhr.open("get","/demo/ajaxDemo",true);
//4.发送请求
xhr.send();
//这里的回调函数是xhr.onreadystatechange绑定的函数,
//在xhr.send()发送请求并拿到响应后执行。
再举一个官方的例子简单看看内部细节:
function myDisplayer(some) {
document.getElementById("demo").innerHTML = some;
}
function myCalculator(num1, num2, myCallback) {
let sum = num1 + num2;
myCallback(sum);
}
myCalculator(5, 5, myDisplayer);
这里myDisplay作为myCallback的参数传入,并在内部数据计算完成后调用。
为什么要先简要介绍回调函数,其实很多时候回调最常与异步函数一起使用。接下来,让我们看看异步任务和异步函数的概念。
1.2异步任务和异步函数
与异步任务相对应的概念是“同步任务”,同步任务在主线程上排队执行,只有前一个任务执行完毕,才能执行下一个任务。
异步任务不进入主线程,而是进入异步队列,前一个任务是否执行完毕不影响下一个任务的执行。
同样,还拿定时器作为异步任务举例:
setTimeout(function(){
console.log('执行了回调函数');
},3000)
console.log('111');
如果按照代码编写的顺序,应该先输出“执行了回调函数”,再输出“111”。但实际输出为:
111
执行了回调函数
这种不阻塞后面任务执行的任务就叫做异步任务。
异步函数
那么什么是异步函数,答案呼之欲出。
与其他函数并行运行的函数称为异步(asynchronous),一个很好的例子是 setTimeout()。
在使用 JavaScript 函数 setTimeout() 时,可以指定超时时执行的回调函数:
setTimeout(myFunction, 3000);
function myFunction() {
document.getElementById("demo").innerHTML = "I love You !!";
}
在上面的示例中,myFunction 被用作回调。
函数(函数名)作为参数传递给 setTimeout()。
3000 是超时前的毫秒数,所以 3 秒后会调用 myFunction()。
但仅仅将函数作为参数传递时,请记住不要使用括号。
- 正确:setTimeout(myFunction, 3000);
- 错误:setTimeout(myFunction(), 3000);
若不将函数的名称作为参数传递给另一个函数,则始终可以传递整个函数:
setTimeout(function() { myFunction("I love You !!!"); }, 3000);
function myFunction(value) {
document.getElementById("demo").innerHTML = value;
}
当我们用函数来加载外部资源(如脚本或文件),则在内容完全加载之前无法使用这些内容。这时就可以使用回调函数。
function myDisplayer(some) {
document.getElementById("demo").innerHTML = some;
}
function getFile(myCallback) {
let req = new XMLHttpRequest();
req.open('GET', "mycar.html");
req.onload = function() {
if (req.status == 200) {
myCallback(this.responseText);
} else {
myCallback("Error: " + req.status);
}
}
req.send();
}
getFile(myDisplayer);
以下是请求的内容:
<img src="img_car.jpg" alt="Nice car" style="width:100%">
<p>A car is a wheeled, self-powered motor vehicle used for transportation.
Most definitions of the term specify that cars are designed to run primarily on roads, to have seating for one to eight people, to typically have four wheels.</p>
<p>(Wikipedia)</p>
两个基本概念我们了解了,接下来终于迎来了本文重点——回调地狱
1.3什么是回调地狱
前面了解了异步任务和异步函数,我们知道了异步任务的存在使得代码不一定能按照我们想要的顺序去执行,而我们为了确保代码按顺序执行,我们可以在第一个异步函数执行完再继续调用第二个异步函数。
setTimeout(function () { //第一层
console.log('武林要以和为贵');
setTimeout(function () { //第二程
console.log('要讲武德');
setTimeout(function () { //第三层
console.log('不要搞窝里斗');
}, 1000)
}, 2000)
}, 3000)
以上代码乍一看是不是有点晕,实际上就是把一个异步函数作为另一个一个异步函数的回调参数传入,可以理解为循环嵌套。
那么这种层层嵌套的情况,我们就叫做回调地狱,不仅代码复杂,可读性和可维护性都得到了大大降低。
那该如何解决回调地狱呢?
接下来让我看看Promise是如何解决该问题的。
二、Promise
Promise 是一种用于处理异步操作的对象,是es6提出的异步编程解决方案。它可以将异步操作封装成一个 Promise 对象,通过 then() 方法来添加回调函数,当异步操作完成时自动执行回调函数。
Promise 语法
let myPromise = new Promise(function(myResolve, myReject) {
// "Producing Code"(可能需要一些时间,生产结果的代码)
myResolve(result value); // 成功时传入的结果
myReject(error object); // 出错时传入的结果
});
// "Consuming Code" (必须等待有一个结果的代码,消费结果)
myPromise.then(
function(value) { /* 成功时的代码 */ },
function(error) { /* 出错时的代码 */ }
);
大家看到这个语法或许还很懵逼,毕竟第一次面基…很突然还不熟悉。
下面来两段代码进行对比,讲一讲我的理解。希望对大家有帮助。
一样借用官方的例子:
仅使用回调的例子
function getFile(myCallback) {
let req = new XMLHttpRequest();
req.open('GET', "mycar.html");
req.onload = function() {
//满足一定条件时调用
if (req.status == 200) {
myCallback(req.responseText);
} else {
myCallback("Error: " + req.status);
}
}
req.send();
}
getFile(myDisplayer);
使用 Promise 的例子
let myPromise = new Promise(function(myResolve, myReject) {
//直接把生产结果的代码放置在这里了
let req = new XMLHttpRequest();
req.open('GET', "mycar.htm");
req.onload = function() {
if (req.status == 200) {
myResolve(req.response);//相对应的myResolve存放成功的结果
} else {
myReject("File not Found");//相对应的myReject存放失败的结果
}
};
req.send();
});
//接下来就是消费结果的.then()了,
//成功和失败两个回调(可以选择性书写写)
myPromise.then(
function(value) {myDisplayer(value);},
function(error) {myDisplayer(error);}
);
原先的getFile函数的异步操作被封装进了promise对象。异步函数具有两个回调,
- 成功:myResolve(result value)
- 出错:myReject(error object)
我们需要把异步操作生成出的结果通过参数传入给他们之一。
再通过.then()去消费
myPromise.then(
function(value) { /* code if successful / },
function(error) { / code if some error */ }
)
值得注意的是:
- then有两个回调参数,成功时的回调和失败时的回调(第一个参数、第二个参数),并且有先后顺序,我们可以使用.catch()方法来接收处理失败时相应的数据。
- 回调函数的参数名(value和error)只是为了语义明显,希望大家注意。
- myResolve()和myReject()是为了传递生产的结果的,可以在任何你传递结果的地方书写,比如:在setTimeout的回调函数内部执行。
let myPromise = new Promise(function(myResolve, myReject) {
setTimeout(function() { myResolve("I love You !!"); }, 3000);
});
myPromise.then(function(value) {
document.getElementById("demo").innerHTML = value;
});
说了这么多,当我们再看到这段代码的时候,我么就有了新的解决方式。
setTimeout(function () { //第一层
console.log('武林要以和为贵');
setTimeout(function () { //第二程
console.log('要讲武德');
setTimeout(function () { //第三层
console.log('不要搞窝里斗');
}, 1000)
}, 2000)
}, 3000)
Promise的链式调用
function fn(str){
var p=new Promise(function(resolve,reject){
//处理异步任务
var flag=true;
setTimeout(function(){
if(flag){
resolve(str)
}
else{
reject('操作失败')
}
})
})
return p;
}
fn('武林要以和为贵')
.then((data)=>{
console.log(data);
return fn('要讲武德');
})
.then((data)=>{
console.log(data);
return fn('不要搞窝里斗')
})
.then((data)=>{
console.log(data);
})
.catch((data)=>{
console.log(data);
})
可能有些同学就有了疑惑,怎么突然能调用这么多.then()啊。
简单说一下Promise的链式调用,它是如何保证代码的执行顺序解决回调地狱的。
实际上就是每一次在前一个Promise对的.then的回调处理完成后,return一个新的Promise对象,这样就能在下一次then时接收到数据(实际上就是新的Promise对象的.then调用)。
最后再介绍一个Promise.all()的用法
Promise.all
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, '3 秒后输出该信息');
});
Promise.all([promise1, promise2, promise3]).then(values => {
console.log(values);
});
// 输出:[1, 2, "3 秒后输出该信息"]
我们将三个 Promise 对象放入了一个数组中,并且使用 Promise.all() 方法来并行处理这些异步操作。当所有异步操作都完成时,then() 方法中设置的回调函数将会被自动执行。
上述代码,一定有同学对Promise.resolve()有疑问
实际上如果一个函数需要返回一个 Promise 对象,
但是实际上不需要进行异步操作,
那么可以使用 Promise.resolve()
来立即返回一个已经有结果生成的 Promise 对象。
- 说到这里应该比较清楚promise的作用了,如果有疑问或者错误可以评论区留言。
但是到这里就结束了吗?明明是不同的异步代码但是一眼看过去全是then…then…then…,说白了还是不够简洁、还是有些复杂,所以下面就顺水推舟引出async/await,可以让代码看起来更像同步代码。
三、Async/Await
"async and await make promises easier to write"
async 使函数返回 Promise
await 使函数等待 Promise
上面是官方的一段话,如果前两节介绍的内容了解了,看到这一段话是不是感觉言简意赅。
- async关键字用于声明一个函数是异步的,它可以在函数定义前使用。async函数内部返回的值会被自动包装成一个Promise。
- await关键字用于等待一个Promise完成(resolve)或拒绝(reject),它可以暂停函数的执行,直到Promise的结果可用。
既然async函数的返回值会被自动包装为一个Promise,所以async函数可以使用.then(),.catch()。
async function fn() {
var flag = true;
if (flag) {
return '不讲武德';
}
else{
throw '处理失败'
}
}
fn()
.then(data=>{
console.log(data);
})
.catch(data=>{
console.log(data);
})
console.log('先执行我,async声明的函数是异步的');
注意以下几点:
- await关键字只能在使用async定义的函数中使用
- await后面可以直接跟一个 Promise实例对象(可以跟任何表达式,更多的是跟一个返回Promise对象的表达式)
- await函数不能单独使用
- await可以直接拿到Promise中resolve中的数据。
当代码执行到async函数中的await时,代码就在此处等待不继续往下执行。当然await只是暂停函数的进一步执行,而不是暂停JavaScript事件循环。
直到await拿到Promise对象中resolve的数据,才继续往下执行,这样就保证了代码的执行顺序,而且使异步代码看起来更像同步代码。
刚刚提到了await可以直接拿到Promise中resolve中的数据,这简直是个神奇。
看看以下例子,await关键字用于等待网络请求完成并将响应转换为JSON格式。如果在这个过程中发生错误,它会被catch块捕获。
async function getAllData() {
try {
//解构赋值
const [user, post] = await Promise.all([
fetch('/api/user'),
fetch('/api/post')
]);
const [userData, postData] = await Promise.all([
user.json(),
post.json()
]);
return { user: userData, post: postData };
} catch (error) {
console.error('Error fetching data:', error);
}
}
getAllData函数使用Promise.all来并发执行两个网络请求,并等待它们都完成。然后,它再次使用Promise.all来并发地将两个响应转换为JSON格式。这种方式使得并发执行异步操作变得非常简单。
有同学可能想问,这个赋值是怎么做到的。这里其实用到了ES6中的解构赋值语法,解构赋值允许我们从数组或对象中提取数据并将其赋给变量。
我会留个链接,稍微晚点更新,你也可以先自行百度。
解构赋值:解构赋值
简单小结
回调函数和异步任务的实际应用出现了回调地狱的问题,而Promise的出现很好的解决了回调地狱的难题。
随着JavaScript的发展,async和await为我们提供了一种更加简洁和直观的方式来编写异步代码。它们不仅使代码更容易阅读和理解,还减少了回调地狱和复杂链式调用带来的困扰。