Promise非同步控制流模式

你的肖同學發表於2018-05-25

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

Node.js 風格函式的 promise 化

在 Javascript 中, 並非所有的非同步控制函式和庫都支援開箱即用的promise,所以在大多數情況下都需要吧一個典型的基於回撥的函式轉換成一個返回promise的函式,比如:

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版本的函式。

順序執行流的迭代模式

在看非同步控制流模式之前,先開始分析順序執行流。按順序執行一組任務意味著一次執行一個任務,一個接一個地執行。執行順序很重要,必須保留,因為列表中任務執行的結果可能影響下一個任務的執行,比如:

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 設計模式(第二版)

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

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);
        });
    });
}
複製程式碼

相關文章