node.js的Promise庫-bluebird示例
原文地址:https://www.cnblogs.com/think8848/p/6591238.html
前兩天公司一哥們寫了一段node.js程式碼發給我,後面特意提了一句“寫的不太優雅”。我知道,他意思是回撥巢狀回撥,因為當時比較急也就沒有再糾結。然而內心中總記得要解決這個問題。解決node.js的回撥金字塔問題有較多方法,在《深入淺出node.js》這本書中介紹了好幾種,有事件釋出/訂閱模式、Promise模式、async庫等。其中Promise模式被很多人推崇,實現的庫有很多,本著從眾的原則,閉著眼睛選個bluebird吧。
然而bluebird的文件並不咋滴,相當不咋滴!網上的例子基本上都是fs.readFile方法的示例,鮮有其他例子。為了更好的理解和使用bluebird,只能自已動手試一下咯。本文字著實用的目的,主要介紹如何將自定義方法轉換為Promise方法,將非同步方法轉換為同步方法呼叫。
1. 首先定義一些簡單的方法,這是一個很簡單例子,模擬讀取配置檔案、開啟資料庫、建立資料庫結構、建立一個使用者、讀取這個使用者、顯示這個使用者屬性的整個過程。此處就不寫node.js的回撥嵌套了,以免使用手機開啟本文時特別慘不忍睹的。
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); });
檢視執行結果,可以看到完全沒有問題,所有方法都按照設想流程在執行。
但是會不會有一種可能,資料太小,電腦執行的很快,所以恰好在下一個方法執行之前上一個方法的非同步已經執行完成了(這之前也遇到過這種問題),我們通過 setTimeout 來驗證一下:把 createUser 方法延遲1000毫秒再執行,看看 getUser 是否還能獲取到資料
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的時候,我有很多疑問。
其中有一個就是:是否僅需將第一個要執行的非同步方法實現為Promise模式,其他的方法只需簡單的放到 .then() 方法即可?我們來進行實驗一下,這裡為了程式碼結構簡單點,我僅演示模擬模擬讀取配置檔案、開啟資料庫、建立資料庫結構、建立一個使用者流程,也很能說明問題了。
"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 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 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() 空方法前導一下