前兩天公司一哥們寫了一段node.js代碼發給我,後面特地提了一句「寫的不太優雅」。我知道,他意思是回調嵌套回調,由於當時比較急也就沒有再糾結。然而心裏中總記得要解決這個問題。解決node.js的回調金字塔問題有較多方法,在《深刻淺出node.js》這本書中介紹了好幾種,有事件發佈/訂閱模式、Promise模式、async庫等。其中Promise模式被不少人推崇,實現的庫有不少,本着從衆的原則,閉着眼睛選個bluebird吧。html
然而bluebird的文檔並不咋滴,至關不咋滴!網上的例子基本上都是fs.readFile方法的示例,鮮有其餘例子。爲了更好的理解和使用bluebird,只能自已動手試一下咯。本文本着實用的目的,主要介紹如何將自定義方法轉換爲Promise方法,將異步方法轉換爲同步方法調用。node
1. 首先定義一些簡單的方法,這是一個很簡單例子,模擬讀取配置文件、打開數據庫、建立數據庫結構、建立一個用戶、讀取這個用戶、顯示這個用戶屬性的整個過程。此處就不寫node.js的回調嵌套了,以避免使用手機打開本文時特別慘不忍睹的。sql
1 //數據庫對象 2 var db; 3 4 //使用配置文件獲取鏈接字符串 5 var getConn = function(cfg){ 6 } 7 8 //建立或打開sqlite3數據庫 9 var openDb = function(dbConn){ 10 } 11 12 //建立數據庫結構 13 var createSchema = function(){ 14 } 15 16 //建立用戶 17 var createUser = function(){ 18 } 19 20 //獲取用戶 21 var getUser = function(id){ 22 } 23 24 //顯示用戶屬性 25 var showUser = function(user){ 26 }
2. 首先來看使用bluebird怎麼將異步方法變成同步方法執行數據庫
"use strict"; var fs = require("fs"); var sqlite3 = require("sqlite3"); var Promise = require("bluebird"); const conn = "conn.txt"; var db; var getConn = function(cfg){ return new Promise(function(resolve, reject){ fs.readFile(cfg, "utf-8", function(err, data){ if(err){ reject(err); } else { console.log("db: ".concat(data)); resolve(data.trim()); } }); }); } var openDb = function(dbConn){ return new Promise(function(resolve, reject){ db = new sqlite3.Database(dbConn, function(err){ if(err){ reject(err); } else{ console.log("open database"); resolve(); } }); }); } var createSchema = function(){ return new Promise(function(resolve, reject){ db.serialize(function(){ var createExpsTable = "CREATE TABLE IF NOT EXISTS expressions ('name' NVARCHAR(20), 'expression' TEXT, 'index' INT, 'likes' INT)"; var createUserTable = "CREATE TABLE IF NOT EXISTS users ('name' NVARCHAR(20), 'password' VARCHAR(20))"; db.exec(createExpsTable, function(err){ if(err){ reject(err); } else { console.log("create table expressions"); } }); db.exec(createUserTable, function(err){ if(err){ reject(err); } else { console.log("create table users"); resolve(); } }); }); }); } var createUser = function(){ return new Promise(function(resolve, reject){ db.run("INSERT INTO users (name, password) VALUES ($name, $password)", {$name: "think8848", $password: "111111"}, function(err){ if(err){ reject(err); } else{ console.log("createUser"); resolve(this.lastID); } }); }); } var getUser = function(id){ return new Promise(function(resolve, reject){ db.get("SELECT rowid, name, password FROM users WHERE rowId = $id", {$id: id}, function(err, row){ if(err){ reject(err); } else { console.log("getUser"); resolve(row); } }); }); } var showUser = function(user){ console.log("id: ".concat(user.rowid).concat(", name: ").concat(user.name).concat(", password: ").concat(user.password)); } getConn(conn) .then(openDb) .then(createSchema) .then(createUser) .then(getUser) .then(showUser) .catch(function(err){ console.log(err.message); });
查看執行結果,能夠看到徹底沒有問題,全部方法都按照設想流程在執行。express
可是會不會有一種可能,數據過小,電腦執行的很快,因此剛好在下一個方法執行以前上一個方法的異步已經執行完成了(這以前也遇到過這種問題),咱們經過 setTimeout 來驗證一下:把 createUser 方法延遲1000毫秒再執行,看看 getUser 是否還能獲取到數據c#
var createUser = function(){ return new Promise(function(resolve, reject){ setTimeout(function(){ console.log("delay 1000ms"); db.run("INSERT INTO users (name, password) VALUES ($name, $password)", {$name: "think8848", $password: "111111"}, function(err){ if(err){ reject(err); } else{ console.log("createUser"); resolve(this.lastID); } }); }, 1000); }); }
查看執行結果,徹底沒有問題, getUser 方法並無偷偷提早執行異步
3. 在剛開始接觸bluebird的時候,我有不少疑問。async
其中有一個就是:是否僅需將第一個要執行的異步方法實現爲Promise模式,其餘的方法只需簡單的放到 .then() 方法便可?咱們來進行實驗一下,這裏爲了代碼結構簡單點,我僅演示模擬模擬讀取配置文件、打開數據庫、建立數據庫結構、建立一個用戶流程,也很能說明問題了。 ide
"use strict"; var fs = require("fs"); var sqlite3 = require("sqlite3"); var Promise = require("bluebird"); const conn = "conn.txt"; var db; var getConn = function(cfg){ return new Promise(function(resolve, reject){ fs.readFile(cfg, "utf-8", function(err, data){ if(err){ reject(err); } else { console.log("db: ".concat(data)); resolve(data.trim()); } }); }); } var openDb = function(dbConn){ db = new sqlite3.Database(dbConn, function (err) { if (err) { throw err; } else { console.log("open database"); } }); } var createSchema = function(){ db.serialize(function () { var createExpsTable = "CREATE TABLE IF NOT EXISTS expressions ('name' NVARCHAR(20), 'expression' TEXT, 'index' INT, 'likes' INT)"; var createUserTable = "CREATE TABLE IF NOT EXISTS users ('name' NVARCHAR(20), 'password' VARCHAR(20))"; db.exec(createExpsTable, function (err) { if (err) { throw err; } else { console.log("create table expressions"); } }); db.exec(createUserTable, function (err) { if (err) { throw err; } else { console.log("create table users"); } }); }); } var createUser = function(){ db.run("INSERT INTO users (name, password) VALUES ($name, $password)", { $name: "think8848", $password: "111111" }, function (err) { if (err) { throw err; } else { console.log("createUser"); } }); } getConn(conn) .then(openDb) .then(createSchema) .then(createUser) .catch(function(err){ console.log(err.message); });
查看執行結果,貌似也沒有問題,所有都按照想像中的順序執行了,是真的嗎?函數
仍是再經過 setTimeout 方法驗證下,若是將建立數據庫結構的時間推遲,是否還能正確建立用戶呢?
var createSchema = function(){ setTimeout(function(){ db.serialize(function () { var createExpsTable = "CREATE TABLE IF NOT EXISTS expressions ('name' NVARCHAR(20), 'expression' TEXT, 'index' INT, 'likes' INT)"; var createUserTable = "CREATE TABLE IF NOT EXISTS users ('name' NVARCHAR(20), 'password' VARCHAR(20))"; db.exec(createExpsTable, function (err) { if (err) { throw err; } else { console.log("create table expressions"); } }); db.exec(createUserTable, function (err) { if (err) { throw err; } else { console.log("create table users"); } }); }); }, 1000); }
查看執行結果:出錯了,提示沒有找到users表,這說明建立用戶方法的執行時間要早於建立數據庫結構的執行時間。這代表若是要確保每一個方法都順序執行,那就必須每一個方法都是Promise模式。
爲了更好的看清楚Promise的執行順序,下面再次用一個簡單的例子和運行結果來展現這個問題
"use strict"; var Promise = require("bluebird"); var first = function(){ console.log("first"); }; var second = function(){ console.log("second"); } var third = function(){ console.log("third"); } Promise.resolve().then(first).then(second).then(third);
查看執行結果
修改 second 方法爲異步方法
var second = function(){ setTimeout(function () { console.log("second"); }, 1000); }
查看執行結果,發現執行順序已經錯了
修改 second 方法爲 Promise 方法
var second = function(){ return new Promise(function(resolve, reject){ setTimeout(function(){ console.log("second"); resolve(); },1000); }); }
查看執行結果,發現順序又和預期同樣了
4. 每一個Promise方法都使用這種寫法好像有點麻煩,是否有更好的辦法呢?在不少bluebird的例子中都給了答案,使用promisify方法,下面咱們來看改造後的例子。這裏值的一提是的,經實驗發現,若是要 promisify 一個方法(這個方法被bluebird官方稱之爲 nodeFunction ),那麼這個方法就必須知足如下簽名: function(any arguments..., function callback) nodeFunction ,即:有兩個參數,第一個參數是上一個Promise執行後的返回值,第二個參數是回調方法,及時上一個方法沒有返回值,那麼第一個參數也是不該該省去的。儘量不要給這個 nodeFunction 方法提供多個參數,若是上一個方法有多個返回值,那麼最好將多個返回值封裝爲一個對象返回。
"use strict"; var fs = require("fs"); var sqlite3 = require("sqlite3"); var Promise = require("bluebird"); const conn = "conn.txt"; var db; var openDb = function(dbConn, callback){ db = new sqlite3.Database(dbConn.trim(), function (err) { if(!err){ console.log("open database"); } return callback(err); }); } var createSchema = function(args, callback){ db.serialize(function () { var createExpsTable = "CREATE TABLE IF NOT EXISTS expressions ('name' NVARCHAR(20), 'expression' TEXT, 'index' INT, 'likes' INT)"; var createUserTable = "CREATE TABLE IF NOT EXISTS users ('name' NVARCHAR(20), 'password' VARCHAR(20))"; db.exec(createExpsTable, function (err) { if(err){ callback(err); } console.log("create table expressions"); }); db.exec(createUserTable, function (err) { if (!err) { console.log("create table users"); } callback(err); }); }); } var createUser = function(args, callback){ db.run("INSERT INTO users (name, password) VALUES ($name, $password)", { $name: "think8848", $password: "111111" }, function (err) { if (!err) { console.log("createUser"); } //此處向下一個Promise方法提供參數值 callback(err, this.lastID); }); } var getUser = function(id, callback){ db.get("SELECT rowid, name, password FROM users WHERE rowId = $id", { $id: id }, function (err, row) { if (!err) { console.log("getUser"); } callback(err, row); }); } var showUser = function(user){ console.log("id: ".concat(user.rowid).concat(", name: ").concat(user.name).concat(", password: ").concat(user.password)); } var getConnAsync = Promise.promisify(fs.readFile); var openDbAsync = Promise.promisify(openDb); var createSchemaAsync = Promise.promisify(createSchema); var createUserAsync = Promise.promisify(createUser); var getUserAsync = Promise.promisify(getUser); getConnAsync(conn, 'utf-8') .then(openDbAsync) .then(createSchemaAsync) .then(createUserAsync) .then(getUserAsync) .then(showUser) .catch(function(err){ console.log(err); });
查看執行結果:徹底沒有問題,妥妥的按照既定的順序來了。
爲了保險,咱們再使用 setTimeout 進行驗證
var createUser = function(args, callback){ setTimeout(function () { console.log("delay 1000ms"); db.run("INSERT INTO users (name, password) VALUES ($name, $password)", { $name: "think8848", $password: "111111" }, function (err) { if (!err) { console.log("createUser"); } //此處向下一個Promise方法提供參數值 callback(err, this.lastID); }); } ,1000); }
驗證結果:能夠看出依舊是按照順序執行的
咱們再看一個例子:
"use strict"; var Promise = require("bluebird"); function first(cb){ var str = "first"; console.log("begin"); cb(null, str); } function second(data,cb){ var str = "second"; console.log(data); cb(null, str); } var firstAsync = Promise.promisify(first); var secondAsync = Promise.promisify(second); firstAsync().then(secondAsync).then(console.log);
其執行結果以下:
仔細觀察咱們會發現這個例子中對兩個方法使用了promisify方法,按照上面的說明,這兩個方法的籤應符合 nodeFunction 約定纔是,然而第一個方法僅包含一個回調函數參數,並無包含值參數,咱們嘗試着加一個:
function first(args, cb){ var str = "first"; console.log("begin"); cb(null, str); }
執行結果以下:驚訝的發現第一個參數是回調函數,而第二個參數爲undefined(此處使用的是vscode的調試功能,畢竟是c#er,感受vscode仍是很是好用)
想都不用想,爲 first 方法提供一個 null 參數確定能解決問題,然而感受實在仍是太奇怪了。
能夠嘗試用稍優雅點的方法來處理,用一個 Promise.resolve() 空方法前導一下