Promise面試題

舞動乾坤發表於2019-03-06

Promise面試題1

有這樣一道關於promise的面試題,描述如下:

頁面上有一個輸入框,兩個按鈕,A按鈕和B按鈕,點選A或者B分別會傳送一個非同步請求,請求完成後,結果會顯示在輸入框中。

題目要求,使用者隨機點選A和B多次,要求輸入框顯示結果時,按照使用者點選的順序顯示,舉例:

使用者點選了一次A,然後點選一次B,又點選一次A,輸入框顯示結果的順序為先顯示A非同步請求結果,再次顯示B的請求結果,最後再次顯示A的請求結果。

UI介面如圖:

Promise面試題

這個需求該如何用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
    })
};
複製程式碼

看到這個題目的時候,腦袋裡瞬間想到了高效率排隊買地鐵票的情景,那個情景類似下圖:

Promise面試題

上圖這樣的排隊和併發請求的場景基本類似,視窗只有三個,人超過三個之後,後面的人只能排隊了。

首先想到的便是利用遞迴來做,就如這篇文章採取的措施一樣,程式碼如下:

//省略程式碼

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)
    }
複製程式碼
  1. 然後,這 5 個非同步請求中無論哪一個先執行完,都會繼續執行下一個list
    let recursion = (arr) => {
        return asyncHandle(arr.shift())
            .then(()=>{
                // 迭代陣列長度不為0, 遞迴執行自身if (arr.length!==0) return recursion(arr) 
                // 迭代陣列長度為0,結束 elsereturn'finish';
            })
    }
複製程式碼
  1. 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)
})
複製程式碼

結果如下:

Promise面試題

手動丟擲異常中斷併發函式測試:

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 手動丟擲異常,停止後續迭代:

Promise面試題

轉載自Promise面試題3控制併發

15 行程式碼實現併發控制(javascript)

相關文章