Promise異步控制流模式

以前的博客在簡書和github.io:https://chijitui.github.io/,但感受簡書好像涼涼了就搬家來掘金了,以後會不按期更關於 Node.js 設計模式和微服務的東西,歡迎投喂點關注,愛您!javascript

Node.js 風格函數的 promise 化

在 Javascript 中, 並不是全部的異步控制函數和庫都支持開箱即用的promise,因此在大多數狀況下都須要吧一個典型的基於回調的函數轉換成一個返回promise的函數,好比:java

function promisify(callbackBaseApi) {
    return function promisified() {
        const args = [].slice.call(arguments);
        return new Promise((resolve, reject) => {
            args.push((err, result) => {
                if(err) {
                    return reject(err);
                }
                if(arguments.length <= 2) {
                    resolve(result);
                } else {
                    resolve([].slice.call(arguments, 1));
                }
            }); 
            callbackBaseApi.apply(null, args);
        });
    }
}
複製代碼

如今的 Node.js 核心實用工具庫util裏面已經支持(err, value) => ...回調函數是最後一個參數的函數, 返回一個返回值是一個promise版本的函數。git

順序執行流的迭代模式

在看異步控制流模式以前,先開始分析順序執行流。按順序執行一組任務意味着一次運行一個任務,一個接一個地運行。執行順序很重要,必須保留,由於列表中任務運行的結果可能影響下一個任務的執行,好比:github

start -> task1 -> task2 -> task3 -> end
複製代碼

這種流程通常都有着幾個特色:算法

  • 按順序執行一組已知任務,而沒有連接或者傳播結果;
  • 一個任務的輸出做爲下一個的輸入;
  • 在每一個元素上運行異步任務時迭代一個集合,一個接一個。

這種執行流直接用在阻塞的 API 中並無太多問題,可是,在咱們使用非阻塞 API 編程的時候就很容易引發回調地獄。好比:編程

task1(err, (callback) => {
    task2(err, (callbakck) => {
        task3(err, (callback) => {
            callback();
        });
    });
});
複製代碼

傳統的解決方案是進行任務的拆解方法就是把每一個任務拆開,經過抽象出一個迭代器,在任務隊列中去順序執行任務:設計模式

class TaskIterator {
    constructor(tasks, callback) {
        this.tasks = tasks;
        this.index = 0;
        this.callback = callback;
    }
    do() {
        if(this.index === this.tasks.length) {
            return this.finish();
        }
        const task = tasks[index];
        task(() => {
            this.index++;
            this.do();
        })
    }
    finish() {
        this.callback();
    }
}

const tasks = [task1, task2, task3];
const taskIterator = new TaskIterator(tasks, callback);

taskIterator.do();
複製代碼

須要注意的是, 若是task()是一個同步操做的話,這樣執行任務就可能變成一個遞歸算法,可能會有因爲再也不每個循環中釋放堆棧而達到調用棧最大限制的風險。數組

順序迭代 模式是很是強大的,由於它能夠適應好幾種狀況。例如,能夠映射數組的值,能夠將操做的結果傳遞給迭代中的下一個,以實現 reduce 算法,若是知足特定條件,能夠提早退出循環,甚至能夠迭代無線數量的元素。 ------ Node.js 設計模式(第二版)promise

值得注意的是,在 ES6 裏,引入了promise以後也能夠更簡便地抽象出順序迭代的模式:bash

const tasks = [task1, task2, task3];

const didTask = tasks.reduce((prev, task) =>{
    return prev.then(() => {
        return task();
    })
}, Promise.resolve());

didTask.then(() => {
    // do callback
});
複製代碼

並行執行流的迭代模式

在一些狀況下,一組異步任務的執行順序並不重要,咱們須要的僅僅是任務完成的時候獲得通知,就能夠用並行執行流程來處理,例如:

-> task1
     /
start -> task2     (allEnd callback)
     \
      -> task3
複製代碼

不做要求的時候,在 Node.js 環境下編程就可開始放飛自我:

class AsyncTaskIterator {
    constructor(tasks, callback) {
        this.tasks = tasks;
        this.callback = callback;
        this.done = 0;
    }
    do() {
        this.tasks.forEach(task => {
            task(() => {
                if(++this.done === this.tasks.length) {
                    this.finish();
                }
            });
        });
    }
    finish() {
        this.callback();
    }
}

const tasks = [task1, task2, task3];
const asyncTaskIterator = new AsyncTaskIterator(tasks, callback);

asyncTaskIterator.do();
複製代碼

使用promise也就能夠經過Promise.all()來接受全部的任務而且執行:

const tasks = [task1, task2, task3];

const didTask = tasks.map(task => task());

Promise.all(didTask).then(() => {
    callback();
})
複製代碼

限制並行執行流的迭代模式

並行編程放飛自我天然是很爽,可是在不少狀況下咱們須要對並行隊列的數量作限制,從而減小資源消耗,好比咱們限制並行隊列最大數爲2

-> task1 -> task2 
     /
start                      (allEnd callback)
     \
      -> task3
複製代碼

這時候,就須要抽象出一個並行隊列,在使用的時候對其實例化:

class TaskQueque {
    constructor(max) {
        this.max = max;
        this.running = 0;
        this.queue = [];
    }
    push(task) {
        this.queue.push(task);
        this.next();
    }
    next() {
        while(this.running < this.max && this.queue.length) {
            const task = this.queue.shift();
            task(() => {
                this.running--;
                this.next();
            });
            this.running++;
        }
    }
}

const tasks = [task1, task2, task3];
const taskQueue = new TaskQueue(2);

let done = 0, hasErrors = false;
tasks.forEach(task => { 
    taskQueue.push(() => {
        task((err) => {
            if(err) {
                hasErrors = true;
                return callback(err);
            }
            if(++done === tasks.length && !hasError) {
                callback();
            }
        });
    });
});
複製代碼

而用promise的處理方式也與之類似:

class TaskQueque {
    constructor(max) {
        this.max = max;
        this.running = 0;
        this.queue = [];
    }
    push(task) {
        this.queue.push(task);
        this.next();
    }
    next() {
        while(this.running < this.max && this.queue.length) {
            const task = this.queue.shift();
            task.then(() => {
                this.running--;
                this.next();
            });
            this.running++;
        }
    }
}

const tasks = [task1, task2, task3];
const taskQueue = new TaskQueue(2);

const didTask = new Promise((resolve, reject) => {
    let done = 0, hasErrors = true;
    tasks.forEach(task => {
        taskQueue.push(() => {
            return task().then(() => {
                if(++done === task.length) {
                    resolve();
                }
            }).catch(err => {
                if(!hasErrors) {
                    hasErrors = true;
                    reject();
                }
            });
        });
    });
});

didTask.then(() => {
    callback();
}).then(err => {
    callback(err);
});
複製代碼

同時暴露兩種類型的 API

那麼問題來了,若是我須要封裝一個庫,使用者須要在callbackpromise中靈活切換怎麼辦呢?讓別人一直本身切換就會顯得很難用,因此就須要同時暴露callbackpromise的 API ,讓使用者傳入callback的時候使用callback,沒有傳入的時候返回一個promise

function asyncDemo(args, callback) {
    return new Promise((resolve, reject) => {
        precess.nextTick(() => {
            // do something 
            // 報錯產出 err, 沒有則產出 result
            if(err) {
                if(callback) {
                    callback(err);
                }
                return resolve(err);
            }
            if(callback){
                callback(null, result);
            }
            resolve(result);
        });
    });
}
複製代碼
相關文章
相關標籤/搜索