我正在参与掘金创作者训练营第 4 期,点击了解活动详情,一起学习吧!
前言
上一篇文章我们已经对 Promise、 setTimeout、then、catch、finally 等相关的面试题,这一篇文章继续带领大家学习 ⬇️ 下面的知识。
💡 阅读本篇文章你可以学到:⬇️
- ✅ Promise 中的 all 和 race
- ✅ async/await 的几道题
- ✅ 几道大厂的面试题
🔊1.Promise 中的 all 和 race
在做题之前,先了解一下 all 和 race 是干嘛用的呢?
🏷️ .all()的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调。
🏷️ .race()的作用也是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。
题 1-1
我们知道如果直接在脚本文件中定义一个 Promise,它构造函数的第一个参数是会立即执行的,就像这样:
const p1 = new Promise((r) => console.log('立即打印'));
控制台中会立即打印出 立即打印”。
因此为了控制它什么时候执行,我们可以用一个函数包裹着它,在需要它执行的时候,调用这个函数就可以了:
function runP1() {
const p1 = newPromise((r) => console.log('立即打印'));
return p1;
}
runP1(); // 调用此函数时才执行
OK 👌, 让我们回归正题。
现在来构建这么一个函数:
function runAsync (x) {
const p = newPromise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
该函数传入一个值x,然后间隔一秒后打印出这个x。
如果我用.all()来执行它会怎样呢?
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)])
.then(res =>console.log(res))
先来想想此段代码在浏览器中会如何执行?
没错,当你打开页面的时候,在间隔一秒后,控制台会同时打印出1, 2, 3,还有一个数组[1, 2, 3]。
1
2
3
[1, 2, 3]
所以你现在能理解这句话的意思了吗:有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据。
.all()后面的.then()里的回调函数接收的就是所有异步操作的结果。
而且这个结果中数组的顺序和Promise.all()接收到的数组顺序一致!!!
使用场景 有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。
题 1-2
我新增了一个runReject函数,它用来在1000 * x秒后reject一个错误。
同时.catch()函数能够捕获到.all()里最先的那个异常,并且只执行一次。
想想这道题会怎样执行呢 🤔️?
题目:
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
function runReject (x) {
const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
return p
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
.then(res =>console.log(res))
.catch(err =>console.log(err))
执行结果:
1
3
// 2s后输出
2
Error: 2
// 4s后输出
4
.catch是会捕获最先的那个异常,在这道题目中最先的异常就是runReject(2)的结果。
另外,如果一组异步操作中有一个异常都不会进入.then()的第一个回调函数参数中。
注意,为什么不说是不进入.then()中呢 🤔️?
哈哈,大家别忘了.then()方法的第二个参数也是可以捕获错误的:
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
.then(res =>console.log(res),
err => console.log(err))
题 1-3
接下来让我们看看.race。
题目:
function runAsync (x) {
const p = newPromise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
.then(res =>console.log('result: ', res))
.catch(err =>console.log(err))
执行结果:
1
'result: ' 1
2
3
使用场景 这个race有什么用呢?使用场景还是很多的,比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作
题 1-4
题目:
function runAsync(x) {
const p = new Promise(r =>
setTimeout(() => r(x, console.log(x)), 1000)
);
return p;
}
function runReject(x) {
const p = new Promise((res, rej) =>
setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
);
return p;
}
Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])
.then(res =>console.log("result: ", res))
.catch(err =>console.log(err));
过程分析: 遇到错误的话,也是一样的,在这道题中,runReject(0)最先执行完,所以进入了catch()中:
执行结果:
0
'Error: 0'
1
2
3
总结
- Promise.all()的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调。
- .race()的作用也是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。
- Promise.all().then()结果中数组的顺序和Promise.all()接收到的数组顺序一致。
🔊2.async await 处理错误
题目2-1
题目:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
console.log('start')
过程分析:
- 首先一进来是创建了两个函数的,我们先不看函数的创建位置,而是看它的调用位置
- 发现async1函数被调用了,然后去看看调用的内容
- 执行函数中的同步代码async1 start,之后碰到了await,它会阻塞async1后面代码的执行,因此会先去执行async2中的同步代码async2,然后跳出async1
- 跳出async1函数后,执行同步代码start
- 在一轮宏任务全部执行完之后,再来执行刚刚await后面的内容async1 end。
执行结果:
'async1 start'
'async2'
'start'
'async1 end'
题目2-2
现在将async结合定时器看看。
题目:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
setTimeout(() => {
console.log('timer')
}, 0)
console.log("async2");
}
async1();
console.log("start")
过程分析:
定时器始终还是最后执行的,它被放到下一条宏任务的延迟队列中。
执行结果:
'async1 start'
'async2'
'start'
'async1 end'
'timer'
题目2-3
这里我们多加几个定时器看看
题目:
asyncfunction async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
setTimeout(() => {
console.log('timer1')
}, 0)
}
asyncfunction async2() {
setTimeout(() => {
console.log('timer2')
}, 0)
console.log("async2");
}
async1();
setTimeout(() => {
console.log('timer3')
}, 0)
console.log("start")
过程分析:
定时器谁先执行,你只需要关注谁先被调用的以及延迟时间是多少,这道题中延迟时间都是0,所以只要关注谁先被调用的。。
执行结果:
'async1 start'
'async2'
'start'
'async1 end'
'timer2'
'timer3'
'timer1'
题目2-4
题目:
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 success');
return'async1 end'
}
console.log('srcipt start')
async1().then(res =>console.log(res))
console.log('srcipt end')
过程分析: 在async1中await后面的Promise是没有返回值的,也就是它的状态始终是pending状态,因此相当于一直在await,await,await却始终没有响应...
所以在await之后的内容是不会执行的,也包括async1后面的 .then。
执行结果:
'script start'
'async1 start'
'promise1'
'script end'
题目2-5
我们再来看一道头条曾经的面试题:
题目:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
});
console.log('script end')
过程分析:
执行结果:
'script start'
'async1 start'
'async2'
'promise1'
'script end'
'async1 end'
'promise2'
'setTimeout'
(这道题最后async1 end和promise2的顺序其实在网上饱受争议,我这里使用浏览器Chrome V80,Node v12.16.1的执行结果都是上面这个答案)
题目2-6
在async中,如果 await后面的内容是一个异常或者错误的话,会怎样呢?
题目:
async function async1 () {
await async2();
console.log('async1');
return'async1 success'
}
async function async2 () {
returnnewPromise((resolve, reject) => {
console.log('async2')
reject('error')
})
}
async1().then(res =>console.log(res))
过程分析:
await后面跟着的是一个状态为rejected的promise。
如果在async函数中抛出了错误,则终止错误结果,不会继续向下执行。
执行结果:
'async2'
Uncaught (in promise) error
题目2-7
如果想要使得错误的地方不影响async函数后续的执行的话,可以使用try catch
题目:
async function async1 () {
try {
awaitPromise.reject('error!!!')
} catch(e) {
console.log(e)
}
console.log('async1');
return Promise.resolve('async1 success')
}
async1().then(res =>console.log(res))
console.log('script start')
执行结果:
'script start'
'error!!!'
'async1'
'async1 success'
🔊3.综合题目
题目3-1
题目:
const first = () =>(new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
console.log(p)
}, 0)
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);
过程分析:
- 第一段代码定义的是一个函数,所以我们得看看它是在哪执行的,发现它在4之前,所以可以来看看first函数里面的内容了。(这一步有点类似于题目1.5)
- 函数first返回的是一个new Promise(),因此先执行里面的同步代码3
- 接着又遇到了一个new Promise(),直接执行里面的同步代码7
- 执行完7之后,在p中,遇到了一个定时器,先将它放到下一个宏任务队列里不管它,接着向下走
- 碰到了resolve(1),这里就把p的状态改为了resolved,且返回值为1,不过这里也先不执行
- 跳出p,碰到了resolve(2),这里的resolve(2),表示的是把first函数返回的那个Promise的状态改了,也先不管它。
- 然后碰到了p.then,将它加入本次循环的微任务列表,等待执行
- 跳出first函数,遇到了first().then(),将它加入本次循环的微任务列表(p.then的后面执行)
- 然后执行同步代码4
- 本轮的同步代码全部执行完毕,查找微任务列表,发现p.then和first().then(),依次执行,打印出1和2
- 本轮任务执行完毕了,发现还有一个定时器没有跑完,接着执行这个定时器里的内容,执行同步代码5
- 然后又遇到了一个resolve(6),它是放在p里的,但是p的状态在之前已经发生过改变了,因此这里就不会再改变,也就是说resolve(6)相- 当于没任何用处,因此打印出来的p为Promise{: 1}。(这一步类似于题目3.1)
执行结果:
3
7
4
1
2
5
Promise{<resolved>: 1}
题目3-2
题目:
const async1 = async () => {
console.log('async1');
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 end')
return'async1 success'
}
console.log('script start');
async1().then(res =>console.log(res));
console.log('script end');
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.catch(4)
.then(res =>console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)
过程分析:
注意的知识点:
async函数中await的new Promise要是没有返回值的话则不执行后面的内容(类似题5.5) .then函数中的参数期待的是函数,如果不是函数的话会发生穿透(类似题3.8 ) 注意定时器的延迟时间
执行结果:
'script start'
'async1'
'promise1'
'script end'
1
'timer2'
'timer1'
🔊4.几道大厂的面试题
题目4-1
题目: 使用Promise实现每隔1秒输出1,2,3
const arr = [1, 2, 3]
arr.reduce((p, x) => {
return p.then(() => {
returnnewPromise(r => {
setTimeout(() => r(console.log(x)), 1000)
})
})
}, Promise.resolve())
题目4-2
题目: 实现mergePromise函数
const time = (timer) => {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, timer)
})
}
const ajax1 = () => time(2000).then(() => {
console.log(1);
return1
})
const ajax2 = () => time(1000).then(() => {
console.log(2);
return2
})
const ajax3 = () => time(1000).then(() => {
console.log(3);
return3
})
function merge Promise () {
// 在这里写代码
}
merge Promise([ajax1, ajax2, ajax3]).then(data => {
console.log("done");
console.log(data); // data 为 [1, 2, 3]
});
// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]
解题思路:
这道题有点类似于Promise.all(),不过.all()不需要管执行顺序,只需要并发执行就行了。但是这里需要等上一个执行完毕之后才能执行下一个。
解题思路:
定义一个数组data用于保存所有异步操作的结果 初始化一个const promise = Promise.resolve(),然后循环遍历数组,在promise后面添加执行ajax任务,同时要将添加的结果重新赋值到promise上。
答案:
function mergePromise (ajaxArray) {
// 存放每个ajax的结果
const data = [];
let promise = Promise.resolve();
ajaxArray.forEach(ajax => {
// 第一次的then为了用来调用ajax
// 第二次的then是为了获取ajax的结果
promise = promise.then(ajax).then(res => {
data.push(res);
return data; // 把每次的结果返回
})
})
// 最后得到的promise它的值就是data
return promise;
}
题目4-3
题目:封装一个异步加载图片的方法
function loadImg(url) {
returnnewPromise((resolve, reject) => {
const img = new Image();
img.onload = function() {
console.log("一张图片加载完成");
resolve(img);
};
img.onerror = function() {
reject(newError('Could not load image at' + url));
};
img.src = url;
});
题目4-4
题目:限制异步操作的并发个数并尽可能快的完成全部
有8个图片资源的url,已经存储在数组urls中。
urls类似于['https://image1.png', 'https://image2.png', ....]
而且已经有一个函数function loadImg,输入一个url链接,返回一个Promise,该Promise在图片下载完成的时候resolve,下载失败则reject。
但有一个要求,任何时刻同时下载的链接数量不可以超过3个。
请写一段代码实现这个需求,要求尽可能快速地将所有图片下载完成。
var urls = [
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting1.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting2.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting3.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting4.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting5.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn6.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn7.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn8.png",
];
function loadImg(url) {
retur nnew Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
console.log("一张图片加载完成");
resolve(img);
};
img.onerror = function() {
reject(newError('Could not load image at' + url));
};
img.src = url;
});
解题思路
既然题目的要求是保证每次并发请求的数量为3,那么我们可以先请求urls中的前面三个(下标为0,1,2),并且请求的时候使用Promise.race()来同时请求,三个中有一个先完成了(例如下标为1的图片),我们就把这个当前数组中已经完成的那一项(第1项)换成还没有请求的那一项(urls中下标为3)。
直到urls已经遍历完了,然后将最后三个没有完成的请求(也就是状态没有改变的Promise)用Promise.all()来加载它们。
答案
function limitLoad(urls, handler, limit) {
let sequence = [].concat(urls); // 复制urls
// 这一步是为了初始化 promises 这个"容器"
let promises = sequence.splice(0, limit).map((url, index) => {
return handler(url).then(() => {
// 返回下标是为了知道数组中是哪一项最先完成
return index;
});
});
// 注意这里要将整个变量过程返回,这样得到的就是一个Promise,可以在外面链式调用
return sequence
.reduce((pCollect, url) => {
return pCollect
.then(() => {
return Promise.race(promises); // 返回已经完成的下标
})
.then(fastestIndex => { // 获取到已经完成的下标
// 将"容器"内已经完成的那一项替换
promises[fastestIndex] = handler(url).then(
() => {
return fastestIndex; // 要继续将这个下标返回,以便下一次变量
}
);
})
.catch(err => {
console.error(err);
});
}, Promise.resolve()) // 初始化传入
.then(() => { // 最后三个用.all来调用
returnPromise.all(promises);
});
}
limitLoad(urls, loadImg, 3)
.then(res => {
console.log("图片全部加载完毕");
console.log(res);
})
.catch(err => {
console.error(err);
});
写在最后
Promise面试篇写到这里就结束了,希望你再未来面试的路上高歌猛进,一路披荆斩棘,拿到自己的心仪的offer。
朽木不雕难成才,知耻后勇凌云志。 我是朽木白,一个热爱分享的程序猿。如果觉得本文对你有帮助,记得点赞 ➕ 关注,说不定哪天就用得上!您的鼓励是我坚持下去的最大动力 ❤️❤️❤️。