Node7.6 開始正式支持 async/await,而 async/await 因爲其能夠以同步形式的代碼書寫異步程序,被喻爲異步調用的天堂。然而 Node 的回調模式在已經根深蒂固,這個被喻爲「回調地獄」的結構形式推進了 Promise 和 ES6 的迅速成型。然而,從地獄到天堂,並不是一步之遙!javascript
async/await 基於 Promise,而不是基於回調,因此要想從回調地獄中解脫出來,首先要把回調實現修改成 Promise 實現——問題來了,Node 這麼多庫函數,還有更多的第三方庫函數都是使用回調實現的,要想所有修改成 Promise 實現,談何容易?html
固然,解決辦法確定是有的,好比 Async 庫經過 async.waterfall()
實現了對深度回調的「扁平」化,固然它不是用 Promise 實現的,可是有它的扁平化工做做爲基礎,再封裝 Promise
就已經簡潔很多了。java
下面是 Async 官方文檔給出的一個示例node
async.waterfall([ function(callback) { callback(null, 'one', 'two'); }, function(arg1, arg2, callback) { // arg1 now equals 'one' and arg2 now equals 'two' callback(null, 'three'); }, function(arg1, callback) { // arg1 now equals 'three' callback(null, 'done'); } ], function (err, result) { // result now equals 'done' });
若是把它封裝成 Promise
也很容易:git
// promiseWaterfall 使用 async.waterfall 處理函數序列 // 並將最終結果封裝成 Promise function promiseWaterfall(series) { return new Promise((resolve, reject) => { async.waterfall(series, function(err, result) { if (err) { reject(err); } else { resolve(result); } }); }); } // 調用示例 promiseWaterfall([ function(callback) { callback(null, "one", "two"); }, function(arg1, arg2, callback) { // arg1 now equals 'one' and arg2 now equals 'two' callback(null, "three"); }, function(arg1, callback) { // arg1 now equals 'three' callback(null, "done"); } ]).then(result => { // result now equals 'done' });
Q 也是一個經常使用的 Promise 庫,提供了一系列的工具函數來處理 Node 式的回調,好比 Q.nfcall()、Q.nfapply()、Q.denodeify() 等。github
其中,Q.denodeify()
,別名 Q.nfbind()
,能夠將一個 Node 回調風格的函數轉換成 Promise 風格的函數。雖然轉換以後的函數返回的不是原生的 Promise
對象,而是 Q 內部實現的一個 Promise
類的對象,咱們能夠稱之爲 Promise alike 對象。api
Q.denodeify()
的用法很簡單,直接對 Node 風格的函數進行封裝便可,下面也是官方文檔中的例子數組
var readFile = Q.nfbind(FS.readFile); readFile("foo.txt", "utf-8").done(function (text) { // do something with text });
這裏須要說明的是,雖然用 Q.denodeify()
封裝的函數返回的是 Promise alike 對象,可是筆者親測它能夠用於 await 運算[注1]
。promise
[注1]
:await 在 MDN 上被描述爲 「operator」,即運算符,因此這裏說 「await 運算」,或者能夠說 「await 表達式」。數據結構
對於 jser 來講,Bluebird 也不陌生。它經過 Promise.promisify() 和 Promise.promisifyAll() 等提供了對 Node 風格函數的轉換,這和上面提到的 Q.denodeify()
相似。注意這裏提到的 Promise
也不是原生的 Promise,而是 bluebird 實現的,一般使用下面的語句引用:
const Promise = require("bluebird").Promise;
爲了和原生 Promise 區別開來,也能夠改成
const BbPromise = require("bluebird").Promise;
Promise.promisifyAll()
相對特殊一些,它接受一個對象做爲參數,將這個對象的全部方法處理成 Promise 風格,固然你也能夠指定一個 filter 讓它只處理特定的方法——具體操做這裏就很少說,參考官方文檔便可。
與 Q.denodeify()
相似,經過 bluebird 的 Promise.promisify()
或 Promise.promisifyAll()
處理事後的函數,返回的也是一個 Promise alike 對象,並且,也能夠用於 await 運算。
ES6 已經提供了原生 Promise 實現,若是隻是爲了「脫離地獄」而去引用一個第三方庫,彷佛有些不值。若是隻須要少許代碼就能夠本身把回調風格封裝成 Promise 風格,幹嗎不本身實現一個?
不妨分析一下,本身寫個 promisify()
須要作些什麼
[1]>
定義 promisify()
promisify()
是一個轉換函數,它的參數是一個回調風格的函數,它的返回值是一個 Promise 風格的函數,因此不論是參數仍是返回值,都是函數
// promisify 的結構 function promisify(func) { return function() { // ... }; }
[2]>
返回的函數須要返回 Promise
對象既然 promisify()
的返回值是一個 Promise 風格的函數,它的返回值應該是一個 Promise
對象,因此
function promisify(func) { return function() { return new Promise((resolve, reject) => { // TODO }); }; }
[3]>
Promise 中調用 func
毋庸置疑,上面的 TODO
部分須要實現對 func
的調用,並根據結果適當的調用 resolve()
和 reject()
。
function promisify(func) { return function() { return new Promise((resolve, reject) => { func((err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); }; }
Node 回調風格的回調函數第一個參數都是錯誤對象,若是爲 null
表示沒有錯誤,因此會有 (err, result) => {}
這樣的回調定義。
[4]>
加上參數上面調用尚未加上對參數的處理。對於 Node 回調風格的函數,一般前面 n 個參數是內部實現須要使用的參數,而最後一個參數是回調函數。使用 ES6 的可變參數和擴展數據語法很容易實現
// 最終實現以下 function promisify(func) { return function(...args) { return new Promise((resolve, reject) => { func(...args, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); }; }
至此,完整的 promisify()
就實現出來了。
[5]>
實現 promisifyArray()
promisifyArray()
用於批量處理一組函數,參數是回調風格的函數列表,返回對應的 Promise 風格函數列表。在實現了 promisify()
的基礎上實現 promisifyArray()
很是容易。
function promisifyArray(list) { return list.map(promisify); }
[6]>
實現 promisifyObject()
promisifyObject()
的實現須要考慮 this
指針的問題,相對比較複雜,並且也不能直接使用上面的 promisify()
。下面是 promisifyObject()
的簡化實現,詳情參考代碼中的註釋。
function promisifyObject(obj, suffix = "Promisified") { // 參照以前的實現,從新實現 promisify。 // 這個函數沒用到外層的局部變量,沒必要實現爲局域函數, // 這裏實現爲局部函數只是爲了組織演示代碼 function promisify(func) { return function(...args) { return new Promise((resolve, reject) => { // 注意調用方式的變化 func.call(this, ...args, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); }; } // 先找出全部方法名稱, // 若是須要過濾能夠考慮本身加 filter 實現 const keys = []; for (const key in obj) { if (typeof obj[key] === "function") { keys.push(key); } } // 將轉換以後的函數仍然附加到原對象上, // 以確保調用的時候,this 引用正確。 // 爲了不覆蓋原函數,加了一個 suffix。 keys.forEach(key => { obj[`${key}${suffix}`] = promisify(obj[key]); }); return obj; }
脫離了地獄,離天堂就不遠了。我在以前的博客 理解 JavaScript 的 async/await 已經說明了 async/await 和 Promise 的關係。而上面已經使用了大量的篇幅實現了回調風格函數向 Promise 風格函數的轉換,因此接下來要作的就是 async/await 實踐。
既然是在 Node 中使用,前面本身實現的 promisify()
、promisifyArray()
和 promisifyObject()
仍是封裝在一個 Node 模塊中比較好。前面已經定義好了三個函數,只須要導出就好
module.exports = { promisify: promisify, promisifyArray: promisifyArray, promisifyObject: promisifyObject }; // 經過解構對象導入 // const {promisify, promisifyArray, promisifyObject} = require("./promisify");
由於三個函數都是獨立的,也能夠導出成數組,
module.exports = [promisify, promisifyArray, promisifyObject]; // 經過解構數組導入 // const [promisify, promisifyArray, promisifyObject] = require("./promisify");
這個模擬的應用場景裏須要進行一個操做,包括4個步驟 (均爲異步操做)
first()
得到一個用戶 IDsecond()
根據用戶 ID 獲取用戶的信息third()
根據用戶 ID 獲取用戶的分數last()
輸出用戶信息和分數其中第 2
、3
步能夠並行。
class User { constructor(id) { this._id = id; this._name = `User_${id}`; } get id() { return this._id; } get name() { return this._name; } get score() { return this._score || 0; } set score(score) { this._score = parseInt(score) || 0; } toString() { return `[#${this._id}] ${this._name}: ${this._score}`; } }
定義一個 toAsync()
來將普通函數模擬成異步函數。能夠少寫幾句 setTimeout()
。
function toAsync(func, ms = 10) { setTimeout(func, ms); }
function first(callback) { toAsync(() => { // 產生一個 1000-9999 的隨機數做爲 ID const id = parseInt(Math.random() * 9000 + 1000); callback(null, id); }); } function second(id, callback) { toAsync(() => { // 根據 id 產生一個 User 對象 callback(null, new User(id)); }); } function third(id, callback) { toAsync(() => { // 根據 id 計算一個分值 // 這個分值在 50-100 之間 callback(null, id % 50 + 50); }); } function last(user, score, callback) { toAsync(() => { // 將分值填入 user 對象 // 輸出這個對象的信息 user.score = score; console.log(user.toString()); if (callback) { callback(null, user); } }); }
固然,還有導出
module.exports = [first, second, third, last];
const [promisify, promisifyArray, promisifyObject] = require("./promisify"); const [first, second, third, last] = promisifyArray(require("./steps")); // 使用 async/await 實現 // 用 node 運行的時候須要 --harmoney_async_await 參數 async function main() { const userId = await first(); // 並行調用要用 Promise.all 將多個並行處理封裝成一個 Promise const [user, score] = await Promise.all([ second(userId), third(userId) ]); last(user, score); } main();