promise經典面試題


上期講了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瀏覽器可以看到下面的運行結果:
1.png
可以看到,所有圖片加載完成,在沒有失敗的情況下,打印出來all

解析:根據規則5,Promise.race方法接受的參數中有一個promise對象返回結果了就會立即觸發成功或者失敗的函數。這里利用這個特性,先將promise隊列循環加入,直到達到限制,等待racerace后又加入一個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 promisethen方法,實現上比較巧妙。

總結

promise對象在JavaScript中的使用相對復雜,因為寫法多變,而且靈活,提供的方法又比較復雜難懂,在ES6普及的今天,使用范圍也廣,所以會高頻的出現在面試過程中。

相關閱讀:

Promise講解

前端異步是什么?哪些情況下會發生異步?

知道html5 Web Worker標准嗎?能實現JavaScript的多線程?

學習如逆水行舟,不進則退,前端技術飛速發展,如果每天不堅持學習,就會跟不上,我會陪着大家,每天堅持推送博文,跟大家一同進步,希望大家能關注我,第一時間收到最新文章。

個人公眾號:長按保存關注


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM