Node7.6 開始正式支援 async/await,而 async/await 由于其可以以同步形式的代碼書寫異步程式,被喻為異步調用的天堂。然而 Node 的回調模式在已經根深蒂固,這個被喻為“回調地獄”的結構形式推動了 Promise 和 ES6 的迅速成型。然而,從地獄到天堂,并非一步之遙!
async/await 基于 Promise,而不是基于回調,是以要想從回調地獄中解脫出來,首先要把回調實作修改為 Promise 實作——問題來了,Node 這麼多庫函數,還有更多的第三方庫函數都是使用回調實作的,要想全部修改為 Promise 實作,談何容易?
使用第三方庫脫離地獄
Async
當然,解決辦法肯定是有的,比如 Async 庫通過
async.waterfall()
實作了對深度回調的“扁平”化,當然它不是用 Promise 實作的,但是有它的扁平化工作作為基礎,再封裝
Promise
就已經簡潔不少了。
下面是 Async 官方文檔給出的一個示例
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
也很容易:
// 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
Q 也是一個常用的 Promise 庫,提供了一系列的工具函數來處理 Node 式的回調,比如 Q.nfcall()、Q.nfapply()、Q.denodeify() 等。
其中,
Q.denodeify()
,别名
Q.nfbind()
,可以将一個 Node 回調風格的函數轉換成 Promise 風格的函數。雖然轉換之後的函數傳回的不是原生的
Promise
對象,而是 Q 内部實作的一個
Promise
類的對象,我們可以稱之為 Promise alike 對象。
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]
。
[注1]
:await 在 MDN 上被描述為 “operator”,即運算符,是以這裡說 “await 運算”,或者可以說 “await 表達式”。
Bluebird
對于 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()
[1]>
promisify()
promisify()
是一個轉換函數,它的參數是一個回調風格的函數,它的傳回值是一個 Promise 風格的函數,是以不管是參數還是傳回值,都是函數
// promisify 的結構
function promisify(func) {
return function() {
// ...
};
}
[2]>
傳回的函數需要傳回 Promise
對象
[2]>
Promise
既然
promisify()
的傳回值是一個 Promise 風格的函數,它的傳回值應該是一個
Promise
對象,是以
function promisify(func) {
return function() {
return new Promise((resolve, reject) => {
// TODO
});
};
}
[3]>
Promise 中調用 func
[3]>
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]>
加上參數
[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()
[5]>
promisifyArray()
promisifyArray()
用于批量處理一組函數,參數是回調風格的函數清單,傳回對應的 Promise 風格函數清單。在實作了
promisify()
的基礎上實作
promisifyArray()
非常容易。
function promisifyArray(list) {
return list.map(promisify);
}
[6]>
promisifyObject()
[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 實踐。
把 promisify 相關函數封裝成子產品
既然是在 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個步驟 (均為異步操作)
-
獲得一個使用者 IDfirst()
-
根據使用者 ID 擷取使用者的資訊second()
-
根據使用者 ID 擷取使用者的分數third()
-
輸出使用者資訊和分數last()
這個場景用到的資料結構定義如下
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}`;
}
}
使用 setTimeout 來模拟異步
function toAsync(func, ms = 10) {
setTimeout(func, ms);
}
以回調風格模拟4個步驟
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];
async/await 實踐
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();