【前端面试】理解 Promise 和 Async
面试碰到这些问题时,这样回答
1、面试官:“你接受免费加班吗?”
程序员:“我上班不要工资。”
面试官:“你开玩笑?”
程序员:“是你先开玩笑的。”
2、程序员:今天我去一家公司面试,面试官说 “给你五秒 让我记住你”
说完我抡圆了给了他一巴掌,打完我就走。
刚到家面试通知就来了
3、面试官:你最擅长什么?
程序员:我擅长在适当的时候说“不行”。
面试官:举个例子。
程序员:不行!
4、面试官:你可以在公司干多久?
程序员:可以干到公司倒闭!
5、面试官:“熟悉哪种语言”。
程序员:“JavaScript”。
面试官:“知道什么叫类么”。
程序员:“我这人实在,工作努力,不知道什么叫累”。
面试官:“知道什么是包?”。
程序员:“我这人实在 平常不带包 也不用公司准备了”。
面试官:“知道什么是继承么”。
程序员:“我是孤儿没什么可以继承的”。
面试官:“知道什么叫对象么?”。
程序员:“知道,不过我工作努力,上进心强,暂时还没有打算找对象”。
据说这是面试时你经常的答案。。。看完这篇文章,让面试不在尴尬!!!
一、Promise
凡事有因必有果,新事物的出现就代表着老的事物不能满足我们的需求了。
Promise 这个新事物就是在这个背景下出现的,而它代替的老事物就是ES6 之前经常被用的 callback(回调函数)。
1)、回调地狱
什么是回调地狱?
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(3);
setTimeout(() => {
console.log(4);
//无限延伸
}, 4000);
}, 3000);
}, 2000);
}, 1000);
一般来说回调地狱就是出现在异步操作中,下一次的操作依赖上一次的结果,一环套一环,套着套着就套的我们头痛难忍,写出了上面的代码。
虽说上面的这种情况不会真实的出现在项目中,但实际开发中为了拿到数据调用多个接口的情况是很常见的,比如要获取图片的路径,需要先发送第一个请求,拿到图片的ID,然通过图片的ID拿到图片的URL,这样第二个请求需要在第一个请求完成后执行;
$.ajax({
url: "a.json",
data: {},
success: function() {
$.ajax({
url: "b.json",
data: {},
success: function() {
$.ajax({
url: "c.json",
data: {},
success: function() {
$.ajax({
url: "d.json",
data: {},
success: function() {
}
})
}
})
}
})
}
})
2)、Promise 解决异步避免回调地狱
Promise 的出现解决了这个问题,先来看看 Promise 怎么解决回调地狱的。
// 链式写法
var P = new Promise(function(resolve ,reject){
if(true){ //请求 成功
resolve('ok' )
}else {
reject( ' error ')
}
});
p. then( function(v){
console .1og(v);
return new Promise(function(reso1ve , reject){
if(true){ //请求成功
resolve('ok' )
}else{
reject( ' error' )
}
});
},function(v){
console.log(v)
}). then(function(v){
console.log(1)
}, function(v){
console.log(2)
}). then(function(v){
console.log(3)
}, function(v){
console.log(4)
})
3)、Promise 基础
Promise 对象用于表示一个异步操作的最终完成 (或失败),及其结果值。Promise 对象是一个代理对象(代理一个值),被代理的值在 Promise 对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的 promise 对象。
4)、Promise 状态
一个 Promise 对象值是未知的,状态是可变的,但是无论怎么变化,它的状态永远处于以下三种之间:
- pending:初始状态,既不是成功,也不是失败。
- fulfilled:意味着操作成功完成。
- rejected:意味着操作失败。
Promise 的状态会发生变化,成功时会从pending -> fulfilled,失败时会从pending -> rejected,但是此过程是不可逆的,也就是不能从另外两个状态变成pending。fulfilled/rejected这两个状态也被称为 settled 状态。
5)、Promise使用
JS 万物皆对象,所以 Promise 也可以被我们new出来。我们通过下面的语法来新建一个 Promise 对象:
new Promise( function(resolve, reject) {...} /* executor */ );
Promise 的构造函数有一个参数 —— 是一个带有两个参数(resolve, reject)的函数,这两个参数分别代表此次异步操作的结果也就是Promise的状态。resolve和reject函数被调用时,分别会将此次 Promise 的状态改成fulfilled或者rejected,一旦异步操作结束,Promise 的最终状态只能是二者之一,如果异步成功,该状态会被resolve函数修改为fullfilled;相反当异步过程中抛出一个错误,那么该状态就会被reject函数改成rejected。
6)、Promise API
Promise 的原型链以及对象本身有一些方法供我们使用,其中最常用也比较有可说性的就是下面这几个:
then —— Promise.prototype.then(onFulfilled, onRejected)
添加解决(fulfillment)和拒绝(rejection)回调到当前 promise, 返回一个新的 promise, 将以回调的返回值来 resolve。
new Promise( (resolve, reject) => {
setTimeout(() => resolve(1), 1000);
}). then(res => {
console.1og(res);
});
new Promise((resolve, reject) => {
setTimeout(() => reject(2), 1000);
}). then(res => {
console.1og(res);
});
可以看到,.then里面拿到的是我们 Promise resolve 过后的数据。并且他还会返回一个 Promise 继续供我们调用,比如:
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
}). then(res => {
console.log(res); //结果1
return res ;
}). then(res => {
console. log(res);
});
then()用法比较简单,大家肯定也经常用,这里其实就知道.then()是可以一直链式调用的,因为它的返回值也是一个 Promise,就可以了。
catch -- Promise.prototype.catch(onRejected)
添加一个拒绝(rejection) 回调到当前 promise, 返回一个新的 promise。当这个回调函数被调用,新 promise 将以它的返回值来 resolve,否则如果当前 promise 进入 fulfilled 状态,则以当前 promise 的完成结果作为新 promise 的完成结果。
new Promise((resolve, reject) => {
setTimeout(() => reject(1), 1000);
}). then(res => {
console.1og( ' then: ' , res);
}). catch(error => {
console.og( 'catch:', error);
});\
all —— Promise.all(iterable)
这个方法返回一个新的 promise 对象,该 promise 对象在 iterable 参数对象里所有的 promise 对象都成功的时候才会触发成功,一旦有任何一个 iterable 里面的 promise 对象失败则立即触发该 promise 对象的失败。这个新的 promise 对象在触发成功状态以后,会把一个包含 iterable 里所有 promise 返回值的数组作为成功回调的返回值,顺序跟 iterable 的顺序保持一致;如果这个新的 promise 对象触发了失败状态,它会把 iterable 里第一个触发失败的 promise 对象的错误信息作为它的失败错误信息。Promise.all 方法常被用于处理多 个promise 对象的状态集合。
这个算是我经常使用的一个 API 了,上面的内容虽然有点长,但是总结起来其实也很简单,大概就是如下三个方面:
第一:接收一个 Promise 对象数组作为参数
function fun1() {
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(1), 1000);
}). then(res => console. log(res));
}
function fun2() {
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(2), 2000);
}). then(res => console. log(res));
}
function fun3() {
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(3), 3000);
}). then(res => console. log(res));
}
Promise .all([fun1, fun2, fun3]);
第二:参数所有回调成功才是成功,返回值数组与参数顺序一致
function fun1() {
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(1), 1000);
}). then(res => console. log(res));
}
function fun2() {
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(2), 2000);
}). then(res => console. log(res));
}
function fun3() {
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(3), 3000);
}). then(res => console. log(res));
}
Promise .all([fun1(), fun2(), fun3()]) . then(res => {
console .1og(res);
});
第三:参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。
function fun1() {
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(1), 1000);
}). then(res => console. log(res));
}
function fun2() {
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(2), 2000);
}). then(res => console. log(res));
}
function fun3() {
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(3), 3000);
}). then(res => console. log(res));
}
Promise .all([fun1(), fun2(), fun3()]) . then(res => {
console .1og(res);
}).catch(error => {
console.log(error);
});
Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来;
常见问题
setTimeout 和 Promise 都是异步操作,那么谁更快呢?
function fun() {
setTimeout(() => console .1og( ' settimeout' ), 0);
new Promise(() => {
console .1og( ' promise');
})
}
image.png
总结:promise 无关顺序更快执行
二、Async/Await
还得再来一遍,新事物的出现就代表着老的事物不能满足我们的需求,ES6 刚出 Promise 来解决异步问题,ES7 就又出了一个 Async/Await(其实官方名字是 async function),看来 Promise 并没有达到大家伙的预期,所以官方就又搞了个更为优雅的异步解决方案。
为什么说它是为了解决 Promise 带来的问题,可以看看 MDN 官网的下面这段话:
async/await 的目的是简化使用多个 promise 时的同步行为,并对一组 Promises 执行某些操作。正如 Promises 类似于结构化回调,async/await 更像结合了 generators 和 promises。
1)、Promise 并不是完美的解决方案
上面提到的那个异步嵌套 setTimeout的例子来说,事实上,大部分人用 Promise 应该并不会像上面的代码那样写,而是下面这样:
new Promise((resolve, reject) =>
setTimeout(() => resolve(1), 1000);
}).then(res => {
console .1og(res);
new Promise((reso1ve, reject) => {
setTimeout(() => resolve(2), 2000);
}).then(res => {
console .1og(res);
new Promise((reso1ve, reject) => {
setTimeout(() => resolve(3), 3000);
}).then(res => {
console .1og(res);
new Promise((reso1ve, reject) => {
setTimeout(() => resolve(4), 4000);
}).then(res => {
console .1og(res);
});
});
})
})
其实 Promise.then() 如果使用过多,依然还是回调地狱,嵌套依然没有消失,所以来说,Promise 并不能称之为完美的异步方案,ES7 提出了 async function,它用来更为优雅的解决异步
function fun1()
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(1), 1000);
}). then(res => console .1og(res));
}
function fun2()
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(2), 2000);
}). then(res => console .1og(res));
}
function fun3()
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(3), 3000);
}). then(res => console .1og(res));
}
function fun4()
return new Promise( (resolve, reject) => {
setTimeout(() => resolve(4), 4000);
}). then(res => console .1og(res));
}
async function fun5() {
await fun1(); //开始执行第一个异步函数
await fun2(); //第一个执行完,开始执行第二个异步函数
await fun3(); //第二个执行完,开始执行第三个异步函数
await fun4(); //第三个执行完,开始执行第四个异步函数
}
fun5();
2)、async function 理解
关于 async function,其实并没有过多的 API,因为它更像是一个高级语法糖,官方文档给出的也更多都是使用示例。在这里,其实我们只需要知道并强调一件事 —— await 关键字用来暂停等待异步函数的执行结束,如果是 Promise,也就是等待它的 settled 状态,并且 await 只能出现在 async function 内部,不可单独使用。
官方给出了一个比较有意思的例子:
// 一个1秒的异步函数
var resolveAfter1Second = function() {
console .1og("starting fast promise");
return new Promise(resolve => {
setTimeout(function() {
resolve("fast");
console .1og("fast promise is done");
}, 1000);
});
}
//一个2秒的异步函数
var resolveAfter2Seconds = function() {
console .1og("starting slow promise");
return new Promise(resolve => {
setTimeout(function() {
resolve("slow");
console .1og("slow promise is done");
}, 2000);
});
}
另一种
//下面这种写法是 一起执行异步函数,只不过因为await 等待导政输出有先后
var concurrentStart = async function() {
console . log( '==CONCURRENT START with awate=');
const slow = resolveAfter2Seconds(); // starts timer immediately
const fast = resolveAfter1second(): // starts timer immediately
// 1. Execution gets here almost instantly
console .1og(await slow): // 2. this runs 2 seconds after 1.
console.log(await fast); // 3. this runs 2 seconds after 1.. imnediately after 2.. since fast is aiready resolved
}
// 下面这种是标准的等待写法
var sequentialStart = asyne functien() {
console. log('==SEQUENTIAL STARTEE');
// 1. Execution gets here alnost instantly
const slow = await resolveAfter2Seconds();
console.1og(slou); // 2. this runs 2 seconds after 1.
const fast = await resolveAfter1second();
console.1og(fast); // 3. this runs 3 seconds after 1.
}
第二种没什么可说的,想象中就是这个样子,因为 await 会暂停等待函数执行完之后再向下执行,因此等待时间不会重叠,先等待2秒执行 slow 后再等待1秒执行 fast。
而第一种
const slow = await resolveAfter25econds();
const fast = await resolveAfterisecond();
console.log(await slow) ;
console.log(await fast);
上面这两个异步函数因为没有 await 关键字,都是立即执行,因此先输出promise start,之后,两个函数延时不同,虽然 slow 先执行,但是是2秒,而 fast 后执行是1秒,先输出fast done再输出slow done。最后,await 关键字发挥作用,虽然 fast 先执行完,但是你还是要等 await slow 完事之后才能 await fast。