?Promise面試實(shí)戰(zhàn)指北
今天給大家分享一篇不錯(cuò)的Promise面試知識(shí)點(diǎn)梳理。說(shuō)不定對(duì)你有一些幫助。
這是作者鼠子的寄語(yǔ):
本文旨在使用一個(gè)易于理解、易于記憶的方式去吃透promise相關(guān)應(yīng)用側(cè)的技術(shù)點(diǎn),從而應(yīng)用于簡(jiǎn)歷和面試中。
比起其他大佬的文章,本文更注重于實(shí)戰(zhàn)性,同時(shí)也會(huì)盡可能的去提高代碼規(guī)范和質(zhì)量(個(gè)人水平受限無(wú)法給出最優(yōu)解)。俗話說(shuō)的好,貪多嚼不爛,想要深入了解更多實(shí)現(xiàn)方法和細(xì)節(jié)的同學(xué)可以補(bǔ)充看更多更加優(yōu)秀的文章。
超時(shí)控制
背景
眾所周知,fetch請(qǐng)求是無(wú)法設(shè)置超時(shí)時(shí)間的,因此我們需要自己去模擬一個(gè)超時(shí)控制。 轉(zhuǎn)盤(pán)問(wèn)題,一個(gè)抽獎(jiǎng)轉(zhuǎn)盤(pán)動(dòng)畫(huà)效果有5秒,但是一般來(lái)說(shuō)向后端請(qǐng)求轉(zhuǎn)盤(pán)結(jié)果只需要不到一秒,因此請(qǐng)求結(jié)果至少得等5秒才能展現(xiàn)給用戶。
問(wèn)題分析
首先,超時(shí)控制比較簡(jiǎn)單,和Promise.race()的思想是類似,或者可以直接使用這個(gè)函數(shù)去解決。
然后,轉(zhuǎn)盤(pán)問(wèn)題如果要答好,需要考慮兩種情況。
轉(zhuǎn)盤(pán)動(dòng)畫(huà)還未完成,請(qǐng)求結(jié)果已經(jīng)拿到了,此時(shí)要等到動(dòng)畫(huà)完成再展示結(jié)果給用戶。 轉(zhuǎn)盤(pán)動(dòng)畫(huà)完成了,請(qǐng)求結(jié)果還未拿到,此時(shí)需要等待結(jié)果返回(可以設(shè)置請(qǐng)求超時(shí)時(shí)間)。
所以,轉(zhuǎn)盤(pán)問(wèn)題更適合用Promise.all()來(lái)解決。
實(shí)戰(zhàn)版源碼
代碼分為多個(gè)版本,從上自下,記憶難度遞增但面試成績(jī)更優(yōu),請(qǐng)按需選擇。
一、基于Promise.race()的超時(shí)控制。
/**
* 超時(shí)控制版本一
*/
/**
* 輔助函數(shù),封裝一個(gè)延時(shí)promise
* @param {number} delay 延遲時(shí)間
* @returns {Promise<any>}
*/
function sleep(delay) {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("timeout")), delay);
});
}
/**
* 將原promise包裝成一個(gè)帶超時(shí)控制的promise
* @param {()=>Promise<any>} requestFn 請(qǐng)求函數(shù)
* @param {number} timeout 最大超時(shí)時(shí)間
* @returns {Promise<any>}
*/
function timeoutPromise(requestFn, timeout) {
return Promise.race([requestFn(), sleep(timeout)]);
}
// ----------下面是測(cè)試用例------------
// 模擬一個(gè)異步請(qǐng)求函數(shù)
function createRequest(delay) {
return () =>
new Promise((resolve) => {
setTimeout(() => {
resolve("done");
}, delay);
});
}
// 超時(shí)的例子
timeoutPromise(createRequest(2000), 1000).catch((error) =>
console.error(error)
);
// 不超時(shí)的例子
timeoutPromise(createRequest(2000), 3000).then((res) => console.log(res));
復(fù)制代碼
二、將promise.race()干掉。
/**
* 超時(shí)控制版本二
*/
/**
* 輔助函數(shù),封裝一個(gè)延時(shí)promise
* @param {number} delay 延遲時(shí)間
* @returns {Promise<any>}
*/
function sleep(delay) {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("timeout")), delay);
});
}
/**
* 將原promise包裝成一個(gè)帶超時(shí)控制的promise
* @param {()=>Promise<any>} requestFn 請(qǐng)求函數(shù)
* @param {number} timeout 最大超時(shí)時(shí)間
* @returns {Promise<any>}
*/
function timeoutPromise(requestFn, timeout) {
const promises = [requestFn(), sleep(timeout)];
return new Promise((resolve, reject) => {
for (const p of promises) {
p.then((res) => resolve(res)).catch((error) => reject(error));
}
});
}
// ----------下面是測(cè)試用例------------
// 模擬一個(gè)異步請(qǐng)求函數(shù)
function createRequest(delay) {
return () =>
new Promise((resolve) => {
setTimeout(() => {
resolve("done");
}, delay);
});
}
// 超時(shí)的例子
timeoutPromise(createRequest(2000), 1000).catch((error) =>
console.error(error)
);
// 不超時(shí)的例子
timeoutPromise(createRequest(2000), 3000).then((res) => console.log(res));
復(fù)制代碼
三、基于Promise.all()的轉(zhuǎn)盤(pán)問(wèn)題(不考慮請(qǐng)求超時(shí)),和上面略有不同的是sleep函數(shù)超時(shí)后Promise從pending態(tài)轉(zhuǎn)到fulfilled態(tài)而不是rejected態(tài)。
/**
* 轉(zhuǎn)盤(pán)問(wèn)題不考慮超時(shí)
*/
/**
* 輔助函數(shù),封裝一個(gè)延時(shí)promise
* @param {number} delay 延遲時(shí)間
* @returns {Promise<any>}
*/
function sleep(delay) {
return new Promise((resolve) => {
setTimeout(() => resolve(delay), delay);
});
}
/**
* 將原promise包裝成一個(gè)轉(zhuǎn)盤(pán)promise
* @param {()=>Promise<any>} requestFn 請(qǐng)求函數(shù)
* @param {number} animationDuration 動(dòng)畫(huà)持續(xù)時(shí)間
* @returns {Promise<any>}
*/
function turntablePromise(requestFn, animationDuration) {
return Promise.all([requestFn(), sleep(animationDuration)]);
}
// ----------下面是測(cè)試用例------------
// 模擬一個(gè)異步請(qǐng)求函數(shù)
function createRequest(delay) {
return () =>
new Promise((resolve) => {
setTimeout(() => {
resolve("done");
}, delay);
});
}
// 請(qǐng)求比轉(zhuǎn)盤(pán)動(dòng)畫(huà)快
turntablePromise(createRequest(2000), 5000).then((res) => console.log(res));
// 請(qǐng)求比轉(zhuǎn)盤(pán)動(dòng)畫(huà)慢
turntablePromise(createRequest(2000), 1000).then((res) => console.error(res));
復(fù)制代碼
四:基于Promise.all()的轉(zhuǎn)盤(pán)問(wèn)題(考慮請(qǐng)求超時(shí)),無(wú)非就是拼刀刀沒(méi)什么亮點(diǎn)。
/**
* 轉(zhuǎn)盤(pán)問(wèn)題考慮超時(shí)
*/
/**
* 將原promise包裝成一個(gè)帶超時(shí)控制的promise
* @param {Promise<any>} request 你的請(qǐng)求
* @param {number} timeout 最大超時(shí)時(shí)間
* @returns {Promise<any>}
*/
function timeoutPromise(request, timeout) {
function sleep(delay) {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("timeout")), delay);
});
}
const promises = [request, sleep(timeout)];
return new Promise((resolve, reject) => {
for (const p of promises) {
p.then((res) => resolve(res)).catch((error) => reject(error));
}
});
}
/**
* 將原promise包裝成一個(gè)轉(zhuǎn)盤(pán)promise
* @param {()=>Promise<any>} requestFn 請(qǐng)求函數(shù)
* @param {number} timeout 超時(shí)時(shí)間
* @param {number} animationDuration 動(dòng)畫(huà)持續(xù)時(shí)間
* @returns {Promise<any>}
*/
function turntablePromise(requestFn, timeout, animationDuration) {
function sleep(delay) {
return new Promise((resolve) => {
setTimeout(() => resolve(delay), delay);
});
}
return Promise.all([timeoutPromise(requestFn(), timeout), sleep(animationDuration)]);
}
// ----------下面是測(cè)試用例------------
// 模擬一個(gè)異步請(qǐng)求函數(shù)
function createRequest(delay) {
return () =>
new Promise((resolve) => {
setTimeout(() => {
resolve("done");
}, delay);
});
}
// 請(qǐng)求比轉(zhuǎn)盤(pán)動(dòng)畫(huà)慢且超時(shí)
turntablePromise(createRequest(2000), 1500, 1000).catch((error) =>
console.error(error)
);
復(fù)制代碼
五:干掉Promise.all(),這版代碼沒(méi)有加什么核心的東西,無(wú)非就是手寫(xiě)一下這個(gè)api,所以留給大家自測(cè)。
取消重復(fù)請(qǐng)求
背景
當(dāng)用戶頻繁點(diǎn)擊一個(gè)搜索Button時(shí),會(huì)在短時(shí)間內(nèi)發(fā)出大量的搜索請(qǐng)求,給服務(wù)器造成一定的壓力,同時(shí)也會(huì)因請(qǐng)求響應(yīng)的先后次序不同而導(dǎo)致渲染的數(shù)據(jù)與預(yù)期不符。這里,我們可以使用防抖來(lái)減小服務(wù)器壓力,但是卻沒(méi)法很好地解決后面的問(wèn)題。
問(wèn)題分析
這個(gè)問(wèn)題的本質(zhì)在于,同一類請(qǐng)求是有序發(fā)出的(根據(jù)按鈕點(diǎn)擊的次序),但是響應(yīng)順序卻是無(wú)法預(yù)測(cè)的,我們通常只希望渲染最后一次發(fā)出請(qǐng)求響應(yīng)的數(shù)據(jù),而其他數(shù)據(jù)則丟棄。因此,我們需要丟棄(或不處理)除最后一次請(qǐng)求外的其他請(qǐng)求的響應(yīng)數(shù)據(jù)。
實(shí)戰(zhàn)版源碼
其實(shí)axios已經(jīng)有了很好的實(shí)踐,大家可以配合阿寶哥的文章來(lái)食用。此處取消promise的實(shí)現(xiàn)借助了上一章節(jié)的技巧,而在axios中因?yàn)樗挟惒蕉际怯蓌hr發(fā)出的,所以axios的實(shí)現(xiàn)中還借助了xhr.abort()來(lái)取消一個(gè)請(qǐng)求。
/**
* 取消請(qǐng)求
*/
function CancelablePromise() {
this.pendingPromise = null;
}
// 包裝一個(gè)請(qǐng)求并取消重復(fù)請(qǐng)求
CancelablePromise.prototype.request = function (requestFn) {
if (this.pendingPromise) {
this.cancel("取消重復(fù)請(qǐng)求");
}
const _promise = new Promise((resolve, reject) => (this.reject = reject));
this.pendingPromise = Promise.race([requestFn(), _promise]);
return this.pendingPromise;
};
// 取消當(dāng)前請(qǐng)求
CancelablePromise.prototype.cancel = function (reason) {
this.reject(new Error(reason));
this.pendingPromise = null;
};
// ----------下面是測(cè)試用例------------
// 模擬一個(gè)異步請(qǐng)求函數(shù)
function createRequest(delay) {
return () =>
new Promise((resolve) => {
setTimeout(() => {
resolve("done");
}, delay);
});
}
const cancelPromise = new CancelablePromise();
// 前四個(gè)請(qǐng)求將被自動(dòng)取消
for (let i = 0; i < 5; i++) {
cancelPromise
.request(createRequest(1000))
.then((res) => console.log(res)) // 最后一個(gè) done
.catch((err) => console.error(err)); // 前四個(gè) error: 取消重復(fù)請(qǐng)求
}
// 設(shè)置一個(gè)定時(shí)器等3s,讓前面的請(qǐng)求都處理完再繼續(xù)測(cè)試
setTimeout(() => {
// 手動(dòng)取消最后一個(gè)請(qǐng)求
cancelPromise
.request(createRequest(1000))
.then((res) => console.log(res))
.catch((err) => console.error(err)); // error:手動(dòng)取消
cancelPromise.cancel("手動(dòng)取消");
}, 3000);
// 設(shè)置一個(gè)定時(shí)器等4s,讓前面的請(qǐng)求都處理完再繼續(xù)測(cè)試
setTimeout(() => {
cancelPromise
.request(createRequest(1000))
.then((res) => console.log(res)) // done
.catch((err) => console.error(err));
}, 4000);
復(fù)制代碼
限制并發(fā)請(qǐng)求數(shù)
背景
一般來(lái)說(shuō),我們不會(huì)刻意去控制請(qǐng)求的并發(fā)。只有在一些場(chǎng)景下可能會(huì)用到,比如,收集用戶的批量操作(每個(gè)操作對(duì)應(yīng)一次請(qǐng)求),待用戶操作完成后一次性發(fā)出。另外,為了減小服務(wù)器的壓力,我們還會(huì)限制并發(fā)數(shù)。
問(wèn)題分析
看上去,Promise.allSettled很適合應(yīng)對(duì)這樣的場(chǎng)景,但是稍微想一下就能發(fā)現(xiàn),它能控制的粒度還是太粗了。首先,它必須等待所有Promise都resolve或reject,其次,如果有并發(fā)限制的話用它來(lái)做還需要分批請(qǐng)求,實(shí)際效率也會(huì)比較低,短木板效應(yīng)很明顯。
實(shí)戰(zhàn)版源碼
/**
* 限制并發(fā)請(qǐng)求數(shù)
*/
/**
* 并發(fā)請(qǐng)求限制并發(fā)數(shù)
* @param {()=>Promise<any> []} requestFns 并發(fā)請(qǐng)求函數(shù)數(shù)組
* @param {numer} limit 限制最大并發(fā)數(shù)
*/
function concurrentRequest(requestFns, limit) {
// 遞歸函數(shù)
function recursion(requestFn) {
requestFn().finally(() => {
if (_requestFns.length > 0) {
recursion(_requestFns.pop());
}
});
}
const _requestFns = [...requestFns];
// 限制最大并發(fā)量
for (let i = 0; i < limit && _requestFns.length > 0; i++) {
recursion(_requestFns.pop());
}
}
// ----------下面是測(cè)試用例------------
// 模擬一個(gè)異步請(qǐng)求函數(shù)
function createRequest(delay) {
return () =>
new Promise((resolve) => {
setTimeout(() => {
resolve("done");
}, delay);
}).then((res) => {
console.log(res);
});
}
const requestFns = [];
for (let i = 0; i < 10; i++) {
requestFns.push(createRequest(1000));
}
concurrentRequest(requestFns, 3);
復(fù)制代碼
管理全局loading態(tài)
背景
當(dāng)我們一個(gè)頁(yè)面或組件涉及到多個(gè)請(qǐng)求時(shí),可能會(huì)對(duì)應(yīng)多個(gè)loading態(tài)的管理。在某些場(chǎng)景下,我們只希望用一個(gè)loading態(tài)去管理所有異步請(qǐng)求,當(dāng)任一存在pending態(tài)的請(qǐng)求時(shí),展示全局loading組件,當(dāng)所有請(qǐng)求都fulfilled或rejected時(shí),隱藏全局loading組件。
問(wèn)題分析
這個(gè)問(wèn)題的關(guān)鍵就是在于我們需要管理所有pending態(tài)的請(qǐng)求,并適時(shí)更新loading態(tài)。
實(shí)戰(zhàn)版源碼
/**
* 管理全局loading態(tài)
*/
function PromiseManager() {
this.pendingPromise = new Set();
this.loading = false;
}
// 給每個(gè)pending態(tài)的promise生成一個(gè)身份標(biāo)志
PromiseManager.prototype.generateKey = function () {
return `${new Date().getTime()}-${parseInt(Math.random() * 1000)}`;
};
PromiseManager.prototype.push = function (...requestFns) {
for (const requestFn of requestFns) {
const key = this.generateKey();
this.pendingPromise.add(key);
requestFn().finally(() => {
this.pendingPromise.delete(key);
this.loading = this.pendingPromise.size !== 0;
});
}
};
// ----------下面是測(cè)試用例------------
// 模擬一個(gè)異步請(qǐng)求函數(shù)
function createRequest(delay) {
return () =>
new Promise((resolve) => {
setTimeout(() => {
resolve("done");
}, delay);
}).then((res) => console.log(res));
}
const manager = new PromiseManager();
// 增加多個(gè)請(qǐng)求
manager.push(createRequest(1000), createRequest(2000), createRequest(3000));
manager.push(createRequest(1500));
// 每秒輪詢loading態(tài),直到loading為false
const id = setInterval(() => {
console.log(manager.loading);
if (!manager.loading) clearInterval(id);
}, 1000);
// 增加多個(gè)請(qǐng)求
manager.push(createRequest(2500));
復(fù)制代碼
加餐
串行化的三種實(shí)現(xiàn)方式
使用串行化的常見(jiàn)場(chǎng)景,請(qǐng)求之間有依賴關(guān)系或時(shí)序關(guān)系,如紅綠燈。
/**
* 串行化的三種實(shí)現(xiàn)
**/
// 法一,遞歸法
function runPromiseInSeq1(requestFns) {
function recursion(requestFns) {
if (requestFns.length === 0) return;
requestFns
.shift()()
.finally(() => recursion(requestFns));
}
const _requestFns = [...requestFns];
recursion(_requestFns);
}
// 法二:迭代法
async function runPromiseInSeq2(requestFns) {
for (const requestFn of requestFns) {
await requestFn();
}
}
// 法三:reduce
function runPromiseInSeq3(requestFns) {
requestFns.reduce((pre, cur) => pre.finally(() => cur()), Promise.resolve());
}
// 模擬一個(gè)異步請(qǐng)求函數(shù)
function createRequest(delay) {
return () =>
new Promise((resolve) => {
setTimeout(() => {
resolve(delay);
}, delay);
}).then((res) => {
console.log(res);
});
}
// 執(zhí)行順序從左至右
const requestFns = [
createRequest(3000),
createRequest(2000),
createRequest(1000),
];
// 串行調(diào)用
runPromiseInSeq1(requestFns);
// runPromiseInSeq2(requestFns);
// runPromiseInSeq3(requestFns);
復(fù)制代碼
20行最簡(jiǎn)異步鏈?zhǔn)秸{(diào)用
這里模擬了Promise的異步鏈?zhǔn)秸{(diào)用,代碼出處見(jiàn)文章。
function Promise(fn) {
this.cbs = [];
const resolve = (value) => {
setTimeout(() => {
this.data = value;
this.cbs.forEach((cb) => cb(value));
});
}
fn(resolve);
}
Promise.prototype.then = function (onResolved) {
return new Promise((resolve) => {
this.cbs.push(() => {
const res = onResolved(this.data);
if (res instanceof Promise) {
res.then(resolve);
} else {
resolve(res);
}
});
});
};