上期講了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普及的今天,使用范圍也廣,所以會高頻的出現在面試過程中。
相關閱讀:
知道html5 Web Worker標准嗎?能實現JavaScript的多線程?
學習如逆水行舟,不進則退,前端技術飛速發展,如果每天不堅持學習,就會跟不上,我會陪着大家,每天堅持推送博文,跟大家一同進步,希望大家能關注我,第一時間收到最新文章。
個人公眾號: