上期講了promise
基本概念和用法,今天結(jié)合上期的內(nèi)容苟弛,講解幾道經(jīng)典的相關(guān)面試題灸促。
promise基本規(guī)則:
1. 首先Promise
構(gòu)造函數(shù)會(huì)立即執(zhí)行异旧,而Promise.then()
內(nèi)部的代碼在當(dāng)次事件循環(huán)的結(jié)尾立即執(zhí)行(微任務(wù))。
2. promise
的狀態(tài)一旦由等待pending
變?yōu)槌晒?code>fulfilled或者失敗rejected
佣谐。那么當(dāng)前promise
被標(biāo)記為完成肚吏,后面則不會(huì)再次改變?cè)摖顟B(tài)。
3. resolve
函數(shù)和reject
函數(shù)都將當(dāng)前Promise
狀態(tài)改為完成狭魂,并將異步結(jié)果罚攀,或者錯(cuò)誤結(jié)果當(dāng)做參數(shù)返回。
4. Promise.resolve(value)
返回一個(gè)狀態(tài)由給定 value 決定的 Promise 對(duì)象雌澄。如果該值是 thenable(即斋泄,帶有 then 方法的對(duì)象),返回的 Promise 對(duì)象的最終狀態(tài)由 then 方法執(zhí)行決定镐牺;否則的話(該 value 為空炫掐,基本類型或者不帶 then 方法的對(duì)象),返回的 Promise 對(duì)象狀態(tài)為 fulfilled,并且將該 value 傳遞給對(duì)應(yīng)的 then 方法睬涧。通常而言募胃,如果你不知道一個(gè)值是否是 Promise 對(duì)象旗唁,使用 Promise.resolve(value) 來(lái)返回一個(gè) Promise 對(duì)象,這樣就能將該 value 以 Promise 對(duì)象形式使用。
5. Promise.all(iterable)/Promise.race(iterable)
簡(jiǎn)單理解痹束,這2個(gè)函數(shù)检疫,是將接收到的promise
列表的結(jié)果返回,區(qū)別是祷嘶,all
是等待所有的promise
都觸發(fā)成功了电谣,才會(huì)返回,而arce
有一個(gè)成功了就會(huì)返回結(jié)果抹蚀。其中任何一個(gè)promise
執(zhí)行失敗了剿牺,都會(huì)直接返回失敗的結(jié)果。
6. promise
對(duì)象的構(gòu)造函數(shù)只會(huì)調(diào)用一次环壤,then
方法和catch
方法都能多次調(diào)用晒来,但一旦有了確定的結(jié)果,再次調(diào)用就會(huì)直接返回結(jié)果郑现。
開始答題
題目一
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);
可以看:規(guī)則一湃崩,promise
構(gòu)造函數(shù)的代碼會(huì)立即執(zhí)行,then
或者reject
里面的代碼會(huì)放入異步微任務(wù)隊(duì)列接箫,在宏任務(wù)結(jié)束后會(huì)立即執(zhí)行攒读。規(guī)則二:promise
的狀態(tài)一旦變更為成功或者失敗,則不會(huì)再次改變辛友,所以執(zhí)行結(jié)果為:1,2,4,3薄扁。而catch
里面的函數(shù)不會(huì)再執(zhí)行。
題目二
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)
})
根據(jù)規(guī)則6废累,promise
的構(gòu)造函數(shù)只會(huì)執(zhí)行一次邓梅,而then
方法可以多次調(diào)用,但是第二次是直接返回結(jié)果邑滨,不會(huì)有異步等待的時(shí)間日缨,所以執(zhí)行結(jié)果是: 過(guò)一秒打印:once,success,success
。
題目三
在瀏覽器上掖看,下面的程序會(huì)一次輸出哪些內(nèi)容匣距?
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);
事件循環(huán):javascript
的執(zhí)行規(guī)則里面有個(gè)事件循環(huán)Event Loot的規(guī)則,在事件循環(huán)中哎壳,異步事件會(huì)放到異步隊(duì)列里面毅待,但是異步隊(duì)列里面又分為宏任務(wù)和微任務(wù),瀏覽器端的宏任務(wù)一般有:script標(biāo)簽,setTimeout,setInterval,setImmediate,requestAnimationFrame
耳峦。微任務(wù)有:MutationObserver,Promise.then catch finally
恩静。宏任務(wù)會(huì)阻塞瀏覽器的渲染進(jìn)程,微任務(wù)會(huì)在宏任務(wù)結(jié)束后立即執(zhí)行,在渲染之前驶乾。
回到題目邑飒,結(jié)果為:'1,2,10,5,6,8,9,3'。你答對(duì)了嗎级乐?如果對(duì)了疙咸,那你基本理解了事件隊(duì)列,微任務(wù)风科,宏任務(wù)了撒轮。
第一步:執(zhí)行宏任務(wù),結(jié)合規(guī)則一贼穆,輸出:1,2,10题山。這時(shí)候事件循環(huán)里面有異步任務(wù)timeOut1,timeOut2,p2.then,p1.then
。
第二步:宏任務(wù)執(zhí)行完后Event Loop
會(huì)去任務(wù)隊(duì)列取異步任務(wù)故痊,微任務(wù)會(huì)優(yōu)先執(zhí)行顶瞳,這時(shí)候會(huì)先后執(zhí)行p2.then,p1.then
,打印5,6愕秫。
第三步:微任務(wù)執(zhí)行完了慨菱,開始宏任務(wù),由于2個(gè)settimeout
等待時(shí)間一樣戴甩,所以會(huì)執(zhí)行先進(jìn)入異步隊(duì)列的timeOut2,先后打臃取:8。執(zhí)行宏任務(wù)的過(guò)程中甜孤,p3.then微任務(wù)進(jìn)入了隊(duì)列协饲,宏任務(wù)執(zhí)行完畢會(huì)執(zhí)行微任務(wù),輸出:9课蔬。之后執(zhí)行timeOut1,輸出:3囱稽。
第四步:結(jié)合規(guī)則6郊尝,由于p2這個(gè)Promise
對(duì)象的執(zhí)行結(jié)果已經(jīng)確定二跋,所以4不會(huì)被打印。
注:在node.js
上輸出結(jié)果并不是這樣的流昏,因?yàn)?code>node.js的事件循環(huán)跟瀏覽器端的有區(qū)別扎即。
題目四
在不使用async/await
的情況下,順序執(zhí)行一組異步代碼函數(shù)况凉,并輸出最后的結(jié)果谚鄙。
在上篇文章中,已經(jīng)講到過(guò)刁绒,利用promise.resolve
結(jié)合reduce
能順序執(zhí)行一組異步函數(shù)闷营。
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));
以上代碼可以封裝成工具來(lái)使用,利用的是規(guī)則4,promise.resolve
函數(shù)的特點(diǎn)傻盟,其中dd
可以是一組同步函數(shù)速蕊,也可以是異步函數(shù)。最后的結(jié)果在result
里面娘赴,異常信息能在最后捕獲规哲。想看更具體的可以查看這篇文章:
promise講解
題目五
順序加載10張圖片,圖片地址已知诽表,但是同時(shí)最多加載3張圖片唉锌,要求用promise
實(shí)現(xiàn)。
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) {
// 當(dāng)前存在的promise隊(duì)列
let promiseMap = {};
// 當(dāng)前索引對(duì)應(yīng)的加載狀態(tài)竿奏,無(wú)論成功袄简,失敗都會(huì)標(biāo)記為true,格式: {0: true, 1: true, 2: true...}
let loadIndexMap = {};
// 當(dāng)前以及加載到的索引泛啸,方便找到下一個(gè)未加載的索引痘番,為了節(jié)省性能,其實(shí)可以不要
let loadIndex = 0;
const loadAImage = function () {
// 所有的資源都進(jìn)入了異步隊(duì)列
if (Object.keys(loadIndexMap).length === urls.length) {
// 所有的資源都加載完畢平痰,或者進(jìn)入加載狀態(tài)汞舱,遞歸結(jié)束
const promiseList = Object.keys(promiseMap).reduce((arr, item) => {arr.push(promiseMap[item]); return arr}, [])
Promise.all(promiseList).then(res => {
// 這里如果沒有加載失敗,就會(huì)在所有加載完畢后執(zhí)行宗雇,如果其中某個(gè)錯(cuò)誤了昂芜,這里的結(jié)果就不準(zhǔn)確,不過(guò)這個(gè)不是題目要求的赔蒲。
console.log('all');
endHandle && endHandle()
}).catch((e) => {
console.log('end:' + e);
})
} else {
// 遍歷泌神,知道里面有3個(gè)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;
}
}
}
// 獲取當(dāng)前正在進(jìn)行的promise列表,利用reduce從promiseMap里面獲取
const promiseList = Object.keys(promiseMap).reduce((arr, item) => {arr.push(promiseMap[item]); return arr}, [])
Promise.race(promiseList).then((index) => {
// 其中一張加載成功舞虱,刪除當(dāng)前promise欢际,讓PromiseList小于limit,開始遞歸矾兜,加載下一張
console.log('end:' + index);
loadIndexMap[index] = true;
delete promiseMap[index];
loadAImage();
}).catch(e => {
// 加載失敗也繼續(xù)
console.log('end:' + e);
loadIndexMap[e] = true;
delete promiseMap[e];
loadAImage();
})
}
}
loadAImage()
}
startLoadImage(urls, 3)
將代碼復(fù)制到chrome瀏覽器可以看到下面的運(yùn)行結(jié)果:
可以看到损趋,所有圖片加載完成,在沒有失敗的情況下椅寺,打印出來(lái)
all
浑槽。
解析:根據(jù)規(guī)則5,Promise.race
方法接受的參數(shù)中有一個(gè)promise
對(duì)象返回結(jié)果了就會(huì)立即觸發(fā)成功或者失敗的函數(shù)返帕。這里利用這個(gè)特性桐玻,先將promise
隊(duì)列循環(huán)加入,直到達(dá)到限制荆萤,等待race
镊靴,race
后又加入一個(gè)promise
,利用遞歸一直循環(huán)這個(gè)過(guò)程,到最后用promise.all
捕獲剩下的圖片加載偏竟。
題目六
寫出下面函數(shù)的執(zhí)行結(jié)果:
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
根據(jù)規(guī)則4算行,Promise.resolve(1)
會(huì)返回一個(gè)promise對(duì)象
并且會(huì)將1當(dāng)做then
的參數(shù)。而.then 或者 .catch 的參數(shù)期望是函數(shù)苫耸,傳入非函數(shù)則會(huì)發(fā)生值穿透州邢。所以最后會(huì)輸出:1。
題目六
如何取消一個(gè)promise
?
剛開始拿到這個(gè)題會(huì)覺得比較蒙褪子,實(shí)際上量淌,我們可以用Promise,race
的特點(diǎn),多個(gè)Promise
有個(gè)狀態(tài)變?yōu)橥瓿上油剩蜁?huì)立馬返回呀枢。
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("請(qǐng)求被攔截了");
一旦開發(fā)者在1秒內(nèi)主動(dòng)調(diào)用obj.resolve
,那么obj.promise
方法就會(huì)被替換成我們自己的方法笼痛,而不會(huì)執(zhí)行let promise
的then
方法裙秋,實(shí)現(xiàn)上比較巧妙。
總結(jié)
promise
對(duì)象在JavaScript
中的使用相對(duì)復(fù)雜缨伊,因?yàn)閷懛ǘ嘧冋蹋异`活,提供的方法又比較復(fù)雜難懂刻坊,在ES6普及的今天枷恕,使用范圍也廣,所以會(huì)高頻的出現(xiàn)在面試過(guò)程中谭胚。
相關(guān)閱讀:
前端異步是什么徐块?哪些情況下會(huì)發(fā)生異步?
知道html5 Web Worker標(biāo)準(zhǔn)嗎灾而?能實(shí)現(xiàn)JavaScript的多線程胡控?
學(xué)習(xí)如逆水行舟,不進(jìn)則退旁趟,前端技術(shù)飛速發(fā)展昼激,如果每天不堅(jiān)持學(xué)習(xí),就會(huì)跟不上轻庆,我會(huì)陪著大家癣猾,每天堅(jiān)持推送博文,跟大家一同進(jìn)步余爆,希望大家能關(guān)注我,第一時(shí)間收到最新文章夸盟。