Promise面試題

Promise面試題1

有這樣一道關於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的執行順序一致了。

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 、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只須要將函數改成帶參數的函數。

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)
    }
複製代碼
  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)
})
複製代碼

結果以下:

手動拋出異常中斷併發函數測試:

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面試題3控制併發

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

相關文章
相關標籤/搜索