ES6中的Promise
ES6中的Promise
JavaScript本身是單線程語言,這在Node.js學習中已經反復強調過,因為單線程,就需要在程序進行IO操作時做“異步執行”,比如最典型的網絡操作——ajax的使用。
在ajax請求網絡時,就是在異步執行這個過程,等到異步工作執行完成,通過事先註冊好的回調函數,異步事件完成立即觸發回調函數,將回調函數拉入到同步執行中。
可見,異步操作會在將來的某個時間點觸發一個事件來調用某個函數(callback)。
傳統異步與回調函數
在ES5的時代,使用回調函數,在異步代碼執行完後,拉回“正軌”(同步到主線程中),例如以下的jquery ajax例子:
$.post("/example/jquery/demo_test_post.asp" ,
{
name:"Donald Duck",
city:"Duckburg"
},
function(data,status){
alert("數據:" + data + "\n狀態:" + status);
});
這段js代碼做了一個post請求,並註冊了請求結束後的回調函數,而這樣寫代碼最大的兩個問題是:1、不美觀,2、回調函數不好重復使用。
特別是在有多個異步操作且互相約束的情況下,就需要在回調函數中繼續使用回調函數,導致callback地獄,寫出一堆括號嵌套的代碼出來。
試試Promise
Promise是ES6新增的標準,在ES5時代,已經有一些黑科技自己去模擬出這個Promise實現。
什麽是Promise?
正如其翻譯過來的字面意思“承諾”,Promise是代碼和代碼,函數和函數之間的承諾。
來看個簡單的Node.js數據庫連接栗子:
1、使用Promise建立一個公用連接池
const mysql = require("mysql")
const conn_info={
host:"localhost",
user:"root",
password:"",
database:"cslginfo"
}
function query(sql){
let pool = mysql.createPool(conn_info);
return new Promise(function(resolve,reject){
pool.getConnection(function(err,conn){
if(err)
reject("error connecting:"+err.stack);
else{
conn.query(sql,function(err,res,fields){
if(err){
reject("error query");
}else{
resolve(res);
}
});
}
})
})
}
這裏使用Promise為異步操作結束後提供一個承諾的函數,意思是異步函數結束時你必須給個結果,要麽獲得了數據,要麽中間出了錯誤,給出錯誤信息
使用resolve表示成功的連接了,使用reject來給出錯誤,這種感覺有點像一個函數可以有多個返回。
resolve將會觸發鏈式操作的then,並將結果註入到函數變量參數中
reject將會觸發鏈式操作的catch,並將結果註入到函數變量參數中
2、連接池的簡單使用
const sql="select * from student_base_info limit 10";
query(sql).then(function(results){
//resolve給出的承諾在then中兌現
console.log(results)
}).catch(function(err){
//reject給出的承諾兌現在catch中
console.log(err)
})
3、更高級!多表查詢!
上面那種情況並不能體現出Promise的特色,只不過是在then中傳入一個function罷了,這和傳統的回調大法傳入function沒有區別呀!
那假設這種場景:我們查詢學校某個學生的基本信息後,有另一張表對應了學生多條學歷經歷,我們查詢某個學生的學歷經歷就是一種多表之間的依賴查詢,第二次查詢學歷經歷需要根據第一次查詢學生的ID。如果還是使用傳統的回調函數就會出現:回調函數中嵌套著下一層回調函數,這是為了等待當前查詢結束觸發下一次查詢導致的,下一次查詢的函數必須放在當次查詢的函數裏面。如果關聯的表較多,代碼寫出來就會相當難看,基本就是這種鳥樣子:
query("select * form xxx where xxx=xxx",function(results){
query("select * from xxx where ID="results[0].id,function(){
......
query("sql",function(){
query("sql",function(){
......
})
})
})
})
//每一次query查詢都依賴上一次查詢的結果,這就很難受了
但Promise的then.then.then鏈式查詢可以將這麽多查詢串成一個“烤串串”,而不必層層嵌套
//多表關聯查詢
query(`select * from student_base_info where 姓名 = ‘黃有為‘`).then(res=>{
return query(`select * from student_school_info where 學號 = ‘${res[0].學號}‘`)
}).then(res=>{
console.log(res);
}).catch(err=>{
console.log(err);
});
像這樣的鏈式代碼就顯得十分優雅舒適了,就好像一個個諾言在一個個往下實現,用專業的術語說就是:Promise的then把異步操作同步化了。
上述js代碼所做的工作是,查詢一個名叫“黃有為”的人,並從另一張表中根據他的學號查出他上學的經歷,最終效果如下所示:
4、別幹傻事!
千萬千萬要註意,不要在then中再嵌套then,這就沒有意義了,還不如寫你的“回調地獄”去!
不要寫出這種代碼來:
關於Promise一些坑
在使用Promise對象時,還需要註意一些坑!筆者在使用時遇到了不少坑!
1、同一個Promise不能多次使用!
例如:
let num = 0;
let p = new Promise(function(resolve){
resolve(num);
})
while(num<10){
p.then(res=>{
console.log(res);
});
num++;
}
p是同一個Promise對象,其中代碼只會執行一次,故執行後:
我們發現resolve所給出的res結果沒有變過,說明之後9次都會輸出第1次的res結果,承諾已經兌現,不再執行!結論:同一個Promise對象只兌現一次“承諾”。
解決方案:Promise工廠函數
對上面代碼稍作修改:
let num = 0;
let p = function(num){
return new Promise(function(resolve){
resolve(num)
});
}
while(num<10){
p(num).then(res=>{
console.log(res);
});
num++;
}
控制臺輸出:
工廠模式除了能解決多個Promise的問題,還為Promise執行提供輸入參數。
2、多個resolve,reject只傳值一次,後面代碼依然要執行?
當多個resolve,reject混合出現在一個邏輯當中,執行到第一個resolve或reject就會返回到then的函數中。但是!註意但是!!!這些resolve,reject之後的代碼依然會執行!也就是說resolve,reject不能看成是return或者throw操作,他返回一些變量但是卻不結束代碼段。做幾個測試,修改1中一些代碼:
return new Promise(function(resolve,reject){
resolve("same");
resolve(num);
reject("error");
throw(new Error("ss"))
console.log("test");
})
上述代碼中test不會打印,因為throw結束了函數,換成return也是一樣的效果,而then實際接收到的變量應該是“same”,後面的resolve並不會覆蓋第一個,reject也不會覆蓋,但是他們都是會執行的!
效果:
如下代碼:
return new Promise(function(resolve,reject){
resolve("same");
resolve(num);
reject("error");
console.log("test");
})
其中的console.log()就是要執行的,效果:
最後,一個小疑問。如果是多層的多對多數據庫查詢呢?
想象下這種場景,我從表1中讀取了10條數據,再依據這10條,每條從表2中讀取相關的10條(最後應該是100條),問題不在於最後到底幾條數據,而在於讀完第一個10條之後不知道如何通過鏈式查詢讀與這10條相關的100條數據,因為這種查詢是一個樹狀查詢,但Promise的then是種鏈式查詢,無論是邏輯上還是物理上都不好通過Promise和then來實現這種查詢,當然你可以通過工廠模式在第一次查詢出10條數據(乃至n條)後生產n個Promise對象繼續往下模擬出樹狀查詢,但這實現起來很麻煩,並且很難管理這麽多的Promise。
最好的辦法是通過ES7中的async和await來完成異步化同步的轉變!
async,await下一章再說,累了,休息了,祝自己生日快樂!
ES6中的Promise