Promise面試題1
有這樣一道關於promise的面試題,描述如下:
頁面上有一個輸入框,兩個按鈕,A按鈕和B按鈕,點選A或者B分別會傳送一個非同步請求,請求完成後,結果會顯示在輸入框中。
題目要求,使用者隨機點選A和B多次,要求輸入框顯示結果時,按照使用者點選的順序顯示,舉例:
使用者點選了一次A,然後點選一次B,又點選一次A,輸入框顯示結果的順序為先顯示A非同步請求結果,再次顯示B的請求結果,最後再次顯示A的請求結果。
UI介面如圖:
這個需求該如何用promise來實現呢?程式碼如下:
//dom元素
var a = document.querySelector("#a")
var b = document.querySelector("#b")
var i = document.querySelector("#ipt");
//全域性變數p儲存promie例項
var P = Promise.resolve();
a.onclick = function(){
//將事件過程包裝成一個promise並通過then鏈連線到
//全域性的Promise例項上,並更新全域性變數,這樣其他點選
//就可以拿到最新的Promies執行鏈
P = P.then(function(){
//then鏈裡面的函式返回一個新的promise例項
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve()
i.value = "a";
},1000)
})
})
}
b.onclick = function(){
P = P.then(function(){
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve()
console.log("b")
i.value = "b"
},2000)
})
})
}
複製程式碼
我們用定時器來模擬非同步請求,仔細於閱讀程式碼我們發現,在全域性我們定義了一個全域性P,P儲存了一個promise的例項。
然後再觀察點選事件的程式碼,使用者每次點選按鈕時,我們在事件中訪問全域性Promise例項,將非同步操作包裝到成新的Promise例項,然後通過全域性Promise例項的then方法來連線這些行為。
連線的時候需要注意,then鏈的函式中必須將新的promise例項進行返回,不然就會執行順序就不正確了。
需要注意的是,then鏈連線完成後,我們需要更新全域性的P變數,只有這樣,其它點選事件才能得到最新的Promise的執行鏈。
這樣每次使用者點選按鈕就不需要關心回撥執行時機了,因為promise的then鏈會按照其連線順序依次執行。
這樣就能保證使用者的點選順序和promise的執行順序一致了。
Promise面試題2
按照要求:
實現 mergePromise 函式,把傳進去的函式陣列按順序先後執行,並且把返回的資料先後放到陣列 data 中。
程式碼如下:
const timeout = ms => new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});
const ajax1 = () => timeout(2000).then(() => {
console.log('1');
return 1;
});
const ajax2 = () => timeout(1000).then(() => {
console.log('2');
return 2;
});
const ajax3 = () => timeout(2000).then(() => {
console.log('3');
return 3;
});
const mergePromise = ajaxArray => {
// 在這裡實現你的程式碼
};
mergePromise([ajax1, ajax2, ajax3]).then(data => {
console.log('done');
console.log(data); // data 為 [1, 2, 3]
});
// 要求分別輸出
// 1
// 2
// 3
// done
// [1, 2, 3]
複製程式碼
分析:
timeout是一個函式,這個函式執行後返回一個promise例項。
ajax1 、ajax2、ajax3 都是函式,不過這些函式有一些特點,執行後都會會返回一個 新的promise例項。
按題目的要求我們只要順序執行這三個函式就好了,然後把結果放到 data 中,但是這些函式裡都是非同步操作,想要按順序執行,然後輸出 1,2,3並沒有那麼簡單,看個例子。
function A() {
setTimeout(function () {
console.log('a');
}, 3000);
}
function B() {
setTimeout(function () {
console.log('b');
}, 1000);
}
A();
B();
// b
// a
複製程式碼
例子中我們是按順序執行的 A,B 但是輸出的結果卻是 b,a 對於這些非同步函式來說,並不會按順序執行完一個,再執行後一個。
這道題主要考察的是Promise 控制非同步流程,我們要想辦法,讓這些函式,一個執行完之後,再執行下一個,程式碼如何實現呢?
// 儲存陣列中的函式執行後的結果
var data = [];
// Promise.resolve方法呼叫時不帶引數,直接返回一個resolved狀態的 Promise 物件。
var sequence = Promise.resolve();
ajaxArray.forEach(function (item) {
// 第一次的 then 方法用來執行陣列中的每個函式,
// 第二次的 then 方法接受陣列中的函式執行後返回的結果,
// 並把結果新增到 data 中,然後把 data 返回。
sequence = sequence.then(item).then(function (res) {
data.push(res);
return data;
});
})
// 遍歷結束後,返回一個 Promise,也就是 sequence, 他的 [[PromiseValue]] 值就是 data,
// 而 data(儲存陣列中的函式執行後的結果) 也會作為引數,傳入下次呼叫的 then 方法中。
return sequence;
複製程式碼
大概思路如下:全域性定義一個promise例項sequence,迴圈遍歷函式陣列,每次迴圈更新sequence,將要執行的函式item通過sequence的then方法進行串聯,並且將執行結果推入data陣列,最後將更新的data返回,這樣保證後面sequence呼叫then方法,如何後面的函式需要使用data只需要將函式改為帶引數的函式。
Promise面試題3
題目是這樣的:
有 8 個圖片資源的 url,已經儲存在陣列 urls 中(即urls = ['http://example.com/1.jpg', …., 'http://example.com/8.jpg']),而且已經有一個函式 function loadImg,輸入一個 url 連結,返回一個 Promise,該 Promise 在圖片下載完成的時候 resolve,下載失敗則 reject。
但是我們要求,任意時刻,同時下載的連結數量不可以超過 3 個。
請寫一段程式碼實現這個需求,要求儘可能快速地將所有圖片下載完成。
已有程式碼如下:
var urls = [
'https://www.kkkk1000.com/images/getImgData/getImgDatadata.jpg',
'https://www.kkkk1000.com/images/getImgData/gray.gif',
'https://www.kkkk1000.com/images/getImgData/Particle.gif',
'https://www.kkkk1000.com/images/getImgData/arithmetic.png',
'https://www.kkkk1000.com/images/getImgData/arithmetic2.gif',
'https://www.kkkk1000.com/images/getImgData/getImgDataError.jpg',
'https://www.kkkk1000.com/images/getImgData/arithmetic.gif',
'https://www.kkkk1000.com/images/wxQrCode2.png'
];
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = function () {
console.log('一張圖片載入完成');
resolve();
}
img.onerror = reject
img.src = url
})
};
複製程式碼
看到這個題目的時候,腦袋裡瞬間想到了高效率排隊買地鐵票的情景,那個情景類似下圖:
上圖這樣的排隊和併發請求的場景基本類似,視窗只有三個,人超過三個之後,後面的人只能排隊了。
首先想到的便是利用遞迴來做,就如這篇文章採取的措施一樣,程式碼如下:
//省略程式碼
var count = 0;
//對載入圖片的函式做處理,計數器疊加計數
function bao(){
count++;
console.log("併發數:",count)
//條件判斷,urls長度大於0繼續,小於等於零說明圖片載入完成
if(urls.length>0&&count<=3){
//shift從陣列中取出連線
loadImg(urls.shift()).then(()=>{
//計數器遞減
count--
//遞迴呼叫
}).then(bao)
}
}
function async1(){
//迴圈開啟三次
for(var i=0;i<3;i++){
bao();
}
}
async1()
複製程式碼
以上是最常規的思路,我將載入圖片的函式loadImg封裝在bao函式內,根據條件判斷,是否傳送請求,請求完成後繼續遞迴呼叫。
以上程式碼所有邏輯都寫在了同一個函式中然後遞迴呼叫,可以優化一下,程式碼如下:
var count = 0; //當前正在進行數
// 封裝請求的非同步函式,增加計數器功能
function request(){
count++;
loadImg(urls.shift()).then(()=>{
count--
}).then(diaodu)
}
// 負責排程的函式
function diaodu(){
if(urls.length>0&&count<=3){
request();
}
}
function async1(){
for(var i=0;i<3;i++){
request();
}
}
async1()
複製程式碼
上面程式碼將一個遞迴函式拆分成兩個,一個函式只負責計數和傳送請求,另外一個負責排程。
這裡的請求既然已經被封裝成了Promise,那麼我們用Promise和saync、await來完成一下,程式碼如下:
//省略程式碼
// 計數器
var count = 0;
// 全域性鎖
var lock = [];
var l = urls.length;
async function bao(){
if(count>=3){
//超過限制利用await和promise進行阻塞;
let _resolve;
await new Promise((resolve,reject)=>{
_resolve=resolve;
// resolve不執行,將其推入lock陣列;
lock.push(_resolve);
});
}
if(urls.length>0){
console.log(count);
count++
await loadImg(urls.shift());
count--;
lock.length&&lock.shift()()
}
}
for (let i = 0; i < l; i++) {
bao();
}
複製程式碼
大致思路是,遍歷執行urls.length長度的請求,但是當請求併發數大於限制時,超過的請求用await結合promise將其阻塞,並且將resolve填充到lock陣列中,繼續執行,併發過程中有圖片載入完成後,從lock中推出一項resolve執行,lock相當於一個叫號機;
以上程式碼可以優化為:
//省略程式碼
// 計數器
var count = 0;
// 全域性鎖
var lock = [];
var l = urls.length;
// 阻塞函式
function block(){
let _resolve;
return new Promise((resolve,reject)=>{
_resolve=resolve;
// resolve不執行,將其推入lock陣列;
lock.push(_resolve);
});
}
// 叫號機
function next(){
lock.length&&lock.shift()()
}
async function bao(){
if(count>=3){
//超過限制利用await和promise進行阻塞;
await block();
}
if(urls.length>0){
console.log(count);
count++
await loadImg(urls.shift());
count--;
next()
}
}
for (let i = 0; i < l; i++) {
bao();
}
複製程式碼
最後一種方案,也是我十分喜歡的,思考好久才明白,大概思路如下:
用 Promise.race來實現,先併發請求3個圖片資源,這樣可以得到 3 個 Promise例項,組成一個陣列promises ,然後不斷的呼叫 Promise.race 來返回最快改變狀態的 Promise,然後從陣列(promises )中刪掉這個 Promise 物件例項,再加入一個新的 Promise例項,直到全部的 url 被取完。
程式碼如下:
//省略程式碼
function limitLoad(urls, handler, limit) {
// 對陣列做一個拷貝
const sequence = [].concat(urls)
let promises = [];
//併發請求到最大數
promises = sequence.splice(0, limit).map((url, index) => {
// 這裡返回的 index 是任務在 promises 的腳標,
//用於在 Promise.race 之後找到完成的任務腳標
return handler(url).then(() => {
return index
});
});
(async function loop() {
let p = Promise.race(promises);
for (let i = 0; i < sequence.length; i++) {
p = p.then((res) => {
promises[res] = handler(sequence[i]).then(() => {
return res
});
return Promise.race(promises)
})
}
})()
}
limitLoad(urls, loadImg, 3)
複製程式碼
第三種方案的巧妙之處,在於使用了Promise.race。並且在迴圈時用then鏈串起了執行順序。
15 行程式碼實現併發控制(javascript)
做過爬蟲的都知道,要控制爬蟲的請求併發量,其實也就是控制其爬取頻率,以免被封IP,還有的就是以此來控制爬蟲應用執行記憶體,否則一下子處理N個請求,記憶體分分鐘會爆。
而 python
爬蟲一般用多執行緒來控制併發,
然而如果是node.js
爬蟲,由於其單執行緒無阻塞性質以及事件迴圈機制,一般不用多執行緒來控制併發(當然node.js
也可以實現多執行緒,此處非重點不再多講),而是更加簡便地直接在程式碼層級上實現併發。
為圖方便,開發者在開發node
爬蟲一般會找一個併發控制的npm包
,然而第三方的模組有時候也並不能完全滿足我們的特殊需求,這時候我們可能就需要一個自己定製版的併發控制函式。
下面我們用15行程式碼實現一個併發控制的函式。
首先,一個基本的併發控制函式,基本要有以下3個引數:
list
{Array} - 要迭代的陣列limit
{number} - 控制的併發數量asyncHandle
{function} - 對list
的每一個項的處理函式
設計
以下以爬蟲為例項進行講解
設計思路其實很簡單,假如併發量控制是 5
1.首先,瞬發 5 個非同步請求,我們就得到了併發的 5 個非同步請求
// limit = 5
while(limit--) {
handleFunction(list)
}
複製程式碼
- 然後,這 5 個非同步請求中無論哪一個先執行完,都會繼續執行下一個
list
項
let recursion = (arr) => {
return asyncHandle(arr.shift())
.then(()=>{
// 迭代陣列長度不為0, 遞迴執行自身if (arr.length!==0) return recursion(arr)
// 迭代陣列長度為0,結束 elsereturn'finish';
})
}
複製程式碼
- 等
list
所有的項迭代完之後的回撥
returnPromise.all(allHandle)
複製程式碼
程式碼
上述步驟組合起來,就是
/**
* @params list {Array} - 要迭代的陣列
* @params limit {Number} - 併發數量控制數
* @params asyncHandle {Function} - 對`list`的每一個項的處理函式,引數為當前處理項,必須 return 一個Promise來確定是否繼續進行迭代
* @return {Promise} - 返回一個 Promise 值來確認所有資料是否迭代完成
*/
let mapLimit = (list, limit, asyncHandle) => {
let recursion = (arr) => {
return asyncHandle(arr.shift())
.then(()=>{
if (arr.length!==0) return recursion(arr) // 陣列還未迭代完,遞迴繼續進行迭代
else return 'finish';
})
};
let listCopy = [].concat(list);
let asyncList = []; // 正在進行的所有併發非同步操作
while(limit--) {
asyncList.push( recursion(listCopy) );
}
return Promise.all(asyncList); // 所有併發非同步操作都完成後,本次併發控制迭代完成
}
複製程式碼
測試demo
模擬一下非同步的併發情況
var dataLists = [1,2,3,4,5,6,7,8,9,11,100,123];
var count = 0;
mapLimit(dataLists, 3, (curItem)=>{
return new Promise(resolve => {
count++
setTimeout(()=>{
console.log(curItem, '當前併發量:', count--)
resolve();
}, Math.random() * 5000)
});
}).then(response => {
console.log('finish', response)
})
複製程式碼
結果如下:
手動丟擲異常中斷併發函式測試:
var dataLists = [1,2,3,4,5,6,7,8,9,11,100,123];
var count = 0;
mapLimit(dataLists, 3, (curItem)=>{
return new Promise((resolve, reject) => {
count++
setTimeout(()=>{
console.log(curItem, '當前併發量:', count--)
if(curItem > 4) reject('error happen')
resolve();
}, Math.random() * 5000)
});
}).then(response => {
console.log('finish', response)
})
複製程式碼
併發控制情況下,迭代到5,6,7 手動丟擲異常,停止後續迭代: