實現一個併發數可變的 Promise.all 靜態方法

實現一個併發數可變的 Promise.all 靜態方法

Promise.all (iterable): The all function returns a new promise which is fulfilled with an array of fulfillment values for the passed promises, or rejects with the reason of the first passed promise that rejects. It resolves all elements of the passed iterable to promises as it runs this algorithm.

Promise.all 靜態方法具備以下特性:數組

  1. 接收一個 Promise 實例的數組或具備 Iterator 接口的對象
  2. 若是元素不是 Promise 對象則使用 Promise.resolve 轉成 Promise 對象
  3. 若是所有成功狀態變爲 resolved,返回值將有序組成一個數組傳給回調
  4. 只要有一個失敗狀態就變爲 rejected,返回值將直接傳遞給回調
  5. 該方法的返回值是一個新的 Promise 對象

下面是 Promise.all 的簡單用法:promise

const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);

Promise.all([p1, p2, p3])
    .then((results) => {
        console.log(results); // [1, 2, 3]
    });
const p1 = Promise.resolve(1);
const p2 = Promise.reject(2);
const p3 = Promise.resolve(3);

Promise.all([p1, p2, p3])
    .then((results) => {
        console.log(results);
    }).catch((e) => {
        console.log(e); // 2
    });

如何實現 Promise.all 靜態方法

Promise.all = Promise.all || function(promises) {
    // 若是實參不是數組則報錯返回
    if (!isArray(promises)) {
        throw new TypeError('You must pass an array to all.');
    }

    // 結果返回一個新的 Promise 實例
    return new Promise(function(resolve, reject) {
        var i = 0,
            result = [],
            len = promises.length,
            count = len

        // 使用閉包記錄數組執行順序
        function resolver(index) {
            return function(value) {
                resolveAll(index, value);
            };
        }

        // 只要有一個失敗狀態就變爲 rejected
        function rejecter(reason) {
            reject(reason);
        }

        // 若是所有成功狀態變爲 resolved
        function resolveAll(index, value) {
            result[index] = value;
            if (--count == 0) {
                resolve(result)
            }
        }

        // 遍歷數組併發執行異步代碼
        for (; i < len; i++) {
            promises[i].then(resolver(i), rejecter);
        }
    });
}

實現一個調度粒度可變的 Promise.all 靜態方法

那麼回到題目的問題:如何實現一個調度粒度可變的 Promise.all 靜態方法呢?這裏首先可能會產生一個疑問就是什麼叫調度粒度可變,實際上很簡單:就是給 all 方法增長一個正整數類型的參數,用來標識傳入的 Promise 實例數組中能夠併發執行的最大個數。

舉例以下,聲明三個不一樣異步時間的 Promise 實例並調用 all 方法,正常狀況下(咱們粗暴的認爲)其執行時間應該爲200ms(固然這是一個錯誤的答案,在這個例子當中咱們僅考慮併發邏輯),而在調度粒度爲1的 all 方法中其執行時間應該爲450ms,在調度粒度爲2的 all 方法中其執行時間應該爲250ms,以此類推。緩存

const p1 = new Promise((resolve) => setTimeout(() => resolve(1), 150));
const p2 = new Promise((resolve) => setTimeout(() => resolve(2), 200));
const p3 = new Promise((resolve) => setTimeout(() => resolve(3), 100));
Promise.all([p1, p2, p3]).then((r) => console.log(r));

在這種狀況下,立刻能夠想到的一個方法是:咱們可使用隊列的數據結構來實現調度粒度爲1的 all 方法數據結構

Promise.all1 = (promises) => {
    if (!isArray(promises)) {
        throw new TypeError('You must pass an array to all.');
    }

    return new Promise((resolve, reject) => {
        const _q = [...promises];
        const result = [];

        function resolver(value) {
            result.push(value);
            next();
        }

        function rejecter(reason) {
            reject(reason);
        }

        function next() {
            if (_q.length) {
                _q.shift().then(resolver, rejecter);
            } else {
                resolve(result);
            }
        }

        next();
    });
}

寫到這兒不難發現,不一樣調度粒度其實是對隊列每次推出的 Promise 實例數量最大值的約束,以及對返回結果的順序索引做出緩存,那麼把代碼進行簡單的修改便可實現預期的功能。閉包

Promise.all2 = (promises, concurrent = promises.length) => {
    if (!Array.isArray(promises)) {
        throw new TypeError('You must pass an array to all.');
    }

    if (concurrent < 1) {
        return Promise.reject();
    }

    return new Promise((resolve, reject) => {
        const queue = [...promises];
        const result = [];
        let total = promises.length;
        let count = concurrent;
        let index = 0;

        function resolver(index) {
            return function(value) {
                resolveAll(index, value);
            };
        }

        function resolveAll(index, value) {
            result[index] = value;
            count++;
            total--;
            next();
            
            if (!total) {
                resolve(result);
            }
        }

        function rejecter(reason) {
            reject(reason);
        }

        function next() {
            while (queue.length && count > 0) {
                count--;
                (queue.shift())().then(resolver(index++), rejecter);
            }
        }

        next();
    });
};
固然這裏有一個比較弔詭的地方!若是咱們按照上面的代碼進行 Promise 實例的聲明,那麼在執行到 all 方法以前它們就已經在併發執行,也就不會有「不一樣調度粒度」之說,因此爲了實現咱們預期的功能須要在 all 方法內部把這些 Promise 實例初始化出來。
function promiseFactory(fn) {
    return function() {
        return new Promise(fn);
    };
}

const p1 = promiseFactory((resolve) => setTimeout(() => resolve(1), 1500));
const p2 = promiseFactory((resolve) => setTimeout(() => resolve(2), 2000));
const p3 = promiseFactory((resolve) => setTimeout(() => resolve(3), 1000));
console.time('hello world!');

Promise.all2([p1, p2, p3], 1).then((r) => {
    console.log(r)
    console.timeEnd('hello world!'); // 4500+ms打印結果
});

Promise.all2([p1, p2, p3], 2).then((r) => {
    console.log(r)
    console.timeEnd('hello world!'); // 2500+ms打印結果
});

Promise.all2([p1, p2, p3]).then((r) => {
    console.log(r)
    console.timeEnd('hello world!'); // 2000+ms打印結果
});

爲了能看出明顯的區別咱們把定時器的時間延長到秒,執行代碼後完美驗證。併發

至此結束。異步

相關文章
相關標籤/搜索