有這樣一道關於promise的面試題,描述以下:javascript
頁面上有一個輸入框,兩個按鈕,A按鈕和B按鈕,點擊A或者B分別會發送一個異步請求,請求完成後,結果會顯示在輸入框中。java
題目要求,用戶隨機點擊A和B屢次,要求輸入框顯示結果時,按照用戶點擊的順序顯示,舉例:node
用戶點擊了一次A,而後點擊一次B,又點擊一次A,輸入框顯示結果的順序爲先顯示A異步請求結果,再次顯示B的請求結果,最後再次顯示A的請求結果。python
UI界面如圖:web
這個需求該如何用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的實例。ajax
而後再觀察點擊事件的代碼,用戶每次點擊按鈕時,咱們在事件中訪問全局Promise實例,將異步操做包裝到成新的Promise實例,而後經過全局Promise實例的then方法來鏈接這些行爲。npm
鏈接的時候須要注意,then鏈的函數中必須將新的promise實例進行返回,否則就會執行順序就不正確了。segmentfault
須要注意的是,then鏈鏈接完成後,咱們須要更新全局的P變量,只有這樣,其它點擊事件才能獲得最新的Promise的執行鏈。數組
這樣每次用戶點擊按鈕就不須要關心回調執行時機了,由於promise的then鏈會按照其鏈接順序依次執行。
這樣就能保證用戶的點擊順序和promise的執行順序一致了。
按照要求:
實現 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 、ajax二、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只須要將函數改成帶參數的函數。
題目是這樣的:
有 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鏈串起了執行順序。
作過爬蟲的都知道,要控制爬蟲的請求併發量,其實也就是控制其爬取頻率,以避免被封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)
}
複製代碼
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); // 全部併發異步操做都完成後,本次併發控制迭代完成
}
複製代碼
模擬一下異步的併發狀況
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 手動拋出異常,中止後續迭代: