上期讲了promise
基本概念和用法,今天结合上期的内容,讲解几道经典的相关面试题。
promise基本规则:
1. 首先Promise
构造函数会立即执行,而Promise.then()
内部的代码在当次事件循环的结尾立即执行(微任务)。
2. promise
的状态一旦由等待pending
变为成功fulfilled
或者失败rejected
。那么当前promise
被标记为完成,后面则不会再次改变该状态。
3. resolve
函数和reject
函数都将当前Promise
状态改为完成,并将异步结果,或者错误结果当做参数返回。
4. Promise.resolve(value)
返回一个状态由给定 value 决定的 Promise 对象。如果该值是 thenable(即,带有 then 方法的对象),返回的 Promise 对象的最终状态由 then 方法执行决定;否则的话(该 value 为空,基本类型或者不带 then 方法的对象),返回的 Promise 对象状态为 fulfilled,并且将该 value 传递给对应的 then 方法。通常而言,如果你不知道一个值是否是 Promise 对象,使用 Promise.resolve(value) 来返回一个 Promise 对象,这样就能将该 value 以 Promise 对象形式使用。
5. Promise.all(iterable)/Promise.race(iterable)
简单理解,这2个函数,是将接收到的promise
列表的结果返回,区别是,all
是等待所有的promise
都触发成功了,才会返回,而arce
有一个成功了就会返回结果。其中任何一个promise
执行失败了,都会直接返回失败的结果。
6. promise
对象的构造函数只会调用一次,then
方法和catch
方法都能多次调用,但一旦有了确定的结果,再次调用就会直接返回结果。
开始答题
题目一
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
reject('error');
})
promise.then(() => {
console.log(3);
}).catch(e => console.log(e))
console.log(4);
可以看:规则一,promise
构造函数的代码会立即执行,then
或者reject
里面的代码会放入异步微任务队列,在宏任务结束后会立即执行。规则二:promise
的状态一旦变更为成功或者失败,则不会再次改变,所以执行结果为:1,2,4,3。而catch
里面的函数不会再执行。
题目二
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('once')
resolve('success')
}, 1000)
})
promise.then((res) => {
console.log(res)
})
promise.then((res) => {
console.log(res)
})
根据规则6,promise
的构造函数只会执行一次,而then
方法可以多次调用,但是第二次是直接返回结果,不会有异步等待的时间,所以执行结果是: 过一秒打印:once,success,success
。
题目三
在浏览器上,下面的程序会一次输出哪些内容?
const p1 = () => (new Promise((resolve, reject) => {
console.log(1);
let p2 = new Promise((resolve, reject) => {
console.log(2);
const timeOut1 = setTimeout(() => {
console.log(3);
resolve(4);
}, 0)
resolve(5);
});
resolve(6);
p2.then((arg) => {
console.log(arg);
});
}));
const timeOut2 = setTimeout(() => {
console.log(8);
const p3 = new Promise(reject => {
reject(9);
}).then(res => {
console.log(res)
})
}, 0)
p1().then((arg) => {
console.log(arg);
});
console.log(10);
事件循环:javascript
的执行规则里面有个事件循环Event Loot的规则,在事件循环中,异步事件会放到异步队列里面,但是异步队列里面又分为宏任务和微任务,浏览器端的宏任务一般有:script标签,setTimeout,setInterval,setImmediate,requestAnimationFrame
。微任务有:MutationObserver,Promise.then catch finally
。宏任务会阻塞浏览器的渲染进程,微任务会在宏任务结束后立即执行,在渲染之前。
回到题目,结果为:'1,2,10,5,6,8,9,3'。你答对了吗?如果对了,那你基本理解了事件队列,微任务,宏任务了。
第一步:执行宏任务,结合规则一,输出:1,2,10。这时候事件循环里面有异步任务timeOut1,timeOut2,p2.then,p1.then
。
第二步:宏任务执行完后Event Loop
会去任务队列取异步任务,微任务会优先执行,这时候会先后执行p2.then,p1.then
,打印5,6。
第三步:微任务执行完了,开始宏任务,由于2个settimeout
等待时间一样,所以会执行先进入异步队列的timeOut2,先后打印:8。执行宏任务的过程中,p3.then微任务进入了队列,宏任务执行完毕会执行微任务,输出:9。之后执行timeOut1,输出:3。
第四步:结合规则6,由于p2这个Promise
对象的执行结果已经确定,所以4不会被打印。
注:在node.js
上输出结果并不是这样的,因为node.js
的事件循环跟浏览器端的有区别。
题目四
在不使用async/await
的情况下,顺序执行一组异步代码函数,并输出最后的结果。
在上篇文章中,已经讲到过,利用promise.resolve
结合reduce
能顺序执行一组异步函数。
const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...dd) => x => dd.reduce(applyAsync, Promise.resolve(x));
const transformData = composeAsync(funca, funcb, funcc, funcd);
transformData(1).then(result => console.log(result,'last result')).catch(e => console.log(e));
以上代码可以封装成工具来使用,利用的是规则4,promise.resolve
函数的特点,其中dd
可以是一组同步函数,也可以是异步函数。最后的结果在result
里面,异常信息能在最后捕获。想看更具体的可以查看这篇文章:
promise讲解
题目五
顺序加载10张图片,图片地址已知,但是同时最多加载3张图片,要求用promise
实现。
const baseUrl = 'http://img.aizhifou.cn/';
const urls = ['1.png', '2.png', '3.png', '4.png', '5.png','6.png', '7.png', '8.png', '9.png', '10.png'];
const loadImg = function (url, i) {
return new Promise((resolve, reject) => {
try {
// 加载一张图片
let image = new Image();
image.onload = function () {
resolve(i)
}
image.onerror = function () {
reject(i)
};
image.src = baseUrl + url;
} catch (e) {
reject(i)
}
})
}
function startLoadImage(urls, limits, endHandle) {
// 当前存在的promise队列
let promiseMap = {};
// 当前索引对应的加载状态,无论成功,失败都会标记为true,格式: {0: true, 1: true, 2: true...}
let loadIndexMap = {};
// 当前以及加载到的索引,方便找到下一个未加载的索引,为了节省性能,其实可以不要
let loadIndex = 0;
const loadAImage = function () {
// 所有的资源都进入了异步队列
if (Object.keys(loadIndexMap).length === urls.length) {
// 所有的资源都加载完毕,或者进入加载状态,递归结束
const promiseList = Object.keys(promiseMap).reduce((arr, item) => {arr.push(promiseMap[item]); return arr}, [])
Promise.all(promiseList).then(res => {
// 这里如果没有加载失败,就会在所有加载完毕后执行,如果其中某个错误了,这里的结果就不准确,不过这个不是题目要求的。
console.log('all');
endHandle && endHandle()
}).catch((e) => {
console.log('end:' + e);
})
} else {
// 遍历,知道里面有3个promise
while (Object.keys(promiseMap).length < limits) {
for (let i = loadIndex; i < urls.length; i++) {
if (loadIndexMap[i] === undefined) {
loadIndexMap[i] = false;
promiseMap[i] = loadImg(urls[i], i);
loadIndex = i;
break;
}
}
}
// 获取当前正在进行的promise列表,利用reduce从promiseMap里面获取
const promiseList = Object.keys(promiseMap).reduce((arr, item) => {arr.push(promiseMap[item]); return arr}, [])
Promise.race(promiseList).then((index) => {
// 其中一张加载成功,删除当前promise,让PromiseList小于limit,开始递归,加载下一张
console.log('end:' + index);
loadIndexMap[index] = true;
delete promiseMap[index];
loadAImage();
}).catch(e => {
// 加载失败也继续
console.log('end:' + e);
loadIndexMap[e] = true;
delete promiseMap[e];
loadAImage();
})
}
}
loadAImage()
}
startLoadImage(urls, 3)
将代码复制到chrome浏览器可以看到下面的运行结果:
可以看到,所有图片加载完成,在没有失败的情况下,打印出来
all
。
解析:根据规则5,Promise.race
方法接受的参数中有一个promise
对象返回结果了就会立即触发成功或者失败的函数。这里利用这个特性,先将promise
队列循环加入,直到达到限制,等待race
,race
后又加入一个promise
,利用递归一直循环这个过程,到最后用promise.all
捕获剩下的图片加载。
题目六
写出下面函数的执行结果:
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
根据规则4,Promise.resolve(1)
会返回一个promise对象
并且会将1当做then
的参数。而.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。所以最后会输出:1。
题目六
如何取消一个promise
?
刚开始拿到这个题会觉得比较蒙,实际上,我们可以用Promise,race
的特点,多个Promise
有个状态变为完成,就会立马返回。
function wrap(p) {
let obj = {};
let p1 = new Promise((resolve, reject) => {
obj.resolve = resolve;
obj.reject = reject;
});
obj.promise = Promise.race([p1, p]);
return obj;
}
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(123);
}, 1000);
});
let obj = wrap(promise);
obj.promise.then(res => {
console.log(res);
});
// obj.resolve("请求被拦截了");
一旦开发者在1秒内主动调用obj.resolve
,那么obj.promise
方法就会被替换成我们自己的方法,而不会执行let promise
的then
方法,实现上比较巧妙。
总结
promise
对象在JavaScript
中的使用相对复杂,因为写法多变,而且灵活,提供的方法又比较复杂难懂,在ES6普及的今天,使用范围也广,所以会高频的出现在面试过程中。
相关阅读:
Promise讲解
前端异步是什么?哪些情况下会发生异步?
知道html5 Web Worker标准吗?能实现JavaScript的多线程?
学习如逆水行舟,不进则退,前端技术飞速发展,如果每天不坚持学习,就会跟不上,我会陪着大家,每天坚持推送博文,跟大家一同进步,希望大家能关注我,第一时间收到最新文章。