Promise的面试题

1.Promise.all 和 Promise.allSettled 有什么区别?

一句话概括Promise.allSettledPromise.all的最大不同:Promise.allSettled永远不会被reject
🌟 Promise.all的痛点
当需要处理多个Promise并行时,大多数情况下Promise.all用起来是非常顺手的,比如下面这样

1
2
3
4
5
6
7
8
9
const delay = n => new Promise(resolve => setTimeout(resolve, n));

const promises = [
delay(100).then(() => 1),
delay(200).then(() => 2),
]

Promise.all(promises).then(values=>console.log(values))
// 最终输出: [1, 2]

可是,是一旦有一个promise出现了异常,被reject了,情况就会变的麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
const promises = [
delay(100).then(() => 1),
delay(200).then(() => 2),
Promise.reject(3)
]

Promise.all(promises).then(values=>console.log(values))
// 最终输出: Uncaught (in promise) 3

Promise.all(promises)
.then(values=>console.log(values))
.catch(err=>console.log(err))
// 加入catch语句后,最终输出:3

尽管能用catch捕获其中的异常,但你会发现其他执行成功的Promise的消息都丢失了,仿佛石沉大海一般。

要么全部成功,要么全部重来,这是Promise.all本身的强硬逻辑,也是痛点的来源,不能说它错,但这的确给Promise.allSettled留下了立足的空间。

使用Promise.allSettled处理上述代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
const promises = [
delay(100).then(() => 1),
delay(200).then(() => 2),
Promise.reject(3)
]

Promise.allSettled(promises).then(values=>console.log(values))
// 最终输出:
// [
// {status: "fulfilled", value: 1},
// {status: "fulfilled", value: 2},
// {status: "rejected", value: 3},
// ]

可以看到所有promise的数据都被包含在then语句中,且每个promise的返回值多了一个status字段,表示当前promise的状态,没有任何一个promise的信息被丢失。

因此,当用Promise.allSettled时,我们只需专注在then语句里,当有promise被异常打断时,我们依然能妥善处理那些已经成功了的promise,不必全部重来。

2.实现mergePromise函数

题目描述:

实现mergePromise函数,把传进去的数组按顺序先后执行,并且把返回的数据先后放到数组data中。

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
const time = (timer) => {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, timer)
})
}
const ajax1 = () => time(2000).then(() => {
console.log(1);
return 1
})
const ajax2 = () => time(1000).then(() => {
console.log(2);
return 2
})
const ajax3 = () => time(1000).then(() => {
console.log(3);
return 3
})

function mergePromise () {
// 在这里写代码
……
}

mergePromise([ajax1, ajax2, ajax3]).then(data => {
console.log("done");
console.log(data); // data 为 [1, 2, 3]
});

// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]

参考答案

解题思路:

  • 定义一个数组data用于保存所有异步操作的结果
  • 初始化一个const promise = Promise.resolve(),然后循环遍历数组,在promise后面添加执行ajax任务,同时要将添加的结果重新赋值到promise上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

3.使用Promise实现红绿灯交替重复亮

题目描述:

参考答案:

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
function red() {
console.log("red");
}
function green() {
console.log("green");
}
function yellow() {
console.log("yellow");
}
const light = function (timer, cb) {
return new Promise(resolve => {
setTimeout(() => {
cb()
resolve()
}, timer)
})
}
const step = function () {
Promise.resolve().then(() => {
return light(3000, red)
}).then(() => {
return light(2000, green)
}).then(() => {
return light(1000, yellow)
}).then(() => {
return step()
})
}

step();

4.使用Promise实现:限制异步操作的并发个数,并尽可能快的完成全部

🌟 题目描述:
有8个图片资源的url,已经存储在数组urls中。
urls类似于[‘https://image1.png', ‘https://image2.png', ….]
而且已经有一个函数function loadImg,输入一个url链接,返回一个Promise,该Promise在图片下载完成的时候resolve,下载失败则reject。
但有一个要求,任何时刻同时下载的链接数量不可以超过3个。
请写一段代码实现这个需求,要求尽可能快速地将所有图片下载完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
console.log("一张图片加载完成");
resolve(img);
};
img.onerror = function() {
reject(new Error('Could not load image at' + url));
};
img.src = url;
});

🌟参考答案:
既然题目的要求是保证每次并发请求的数量为3,那么我们可以先请求urls中的前面三个(下标为0,1,2),并且请求的时候使用Promise.race()来同时请求,三个中有一个先完成了,我们就把这个当前数组中已经完成的那一项(第1项)换成还没有请求的那一项(urls中下标为3)。

直到urls已经遍历完了,然后将最后三个没有完成的请求(也就是状态没有改变的Promise)用Promise.all()来加载它们。

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
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来调用
return Promise.all(promises);
});
}
limitLoad(urls, loadImg, 3)
.then(res => {
console.log("图片全部加载完毕");
console.log(res);
})
.catch(err => {
console.error(err);
});

5.实现有并行限制的Promise调度器

参考答案:

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
class Scheduler {
constructor() {
this.queue = [];
this.maxCount = 2;
this.runCounts = 0;
}
add(promiseCreator) {
this.queue.push(promiseCreator);
}
taskStart() {
for (let i = 0; i < this.maxCount; i++) {
this.request();
}
}
request() {
if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {
return;
}
this.runCounts++;

this.queue.shift()().then(() => {
this.runCounts--;
this.request();
});
}
}

const timeout = time => new Promise(resolve => {
setTimeout(resolve, time);
})

const scheduler = new Scheduler();

const addTask = (time,order) => {
scheduler.add(() => timeout(time).then(()=>console.log(order)))
}


addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
scheduler.taskStart()
// 2
// 3
// 1
// 4

6.promisify

🌰 适用场景:

  • 原生回调函数 callBack:存在回调地狱问题
  • 函数加上返回 Promise 对象构造成异步函数:每次都需要重新构造,重复工作
  • 使用 Promisify 做 Promise 的封装:减少构造 Promise 的重复工作
1
2
3
4
5
6
7
8
9
10
11
12
13
function promisify(func) {
return function (...args) {
return new Promise( (resolve, reject) => {
let callback = function(...args) {
resolve(args)
}
// 给func函数主动塞入一个callback,这样在func中调用callback的时候实际执行的时候就是
// 我们这里定义的callback函数,然后在我们的callback中调用resolve,
// 这样一来,本来想要通过回调执行的操作就可以放在then函数中进行执行了
func.apply(null, [...args, callback])
})
}
}

验证:

1
2
3
4
5
6
7
8
9
10
11
function hello(v){
setTimeout(()=>{
console.log(v+1)
},1000)
}
//一般调用方式
hello(2)

//实现一个promisefy函数,使得能使用以下方式调用
var newFn = new promisify(hello)
newFn(2).then((v)=>console.log(v))

结果:

文章作者: qinwei
文章链接: https://qw-null.github.io/2022/09/19/Promise的手写题/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 QW's Blog