前言
對于Node.js的異步控制流,目前共計有四種常用的方式。較為經典的為callback
和EventEmitter
;在ES6中,加入了Promise
;在ES7中加入了async/await
。下面就逐個分析一下這四種常用的異步控制。
callback形式的異步控制
對于callback
形式,即采用回調函數。在理解上,就是函數將任務分配出去,當任務完成之后,然后根據執行結果來進行相應的回調。可以看下面一個例子:
function sleep(ms, callback){
setTimeout(function(){
callback("finish") //執行完之后,返回‘finish’。
}, ms);
}
sleep(1000, function(val){
console.log(val);
});
//輸出結果:finish。
這種形式十分容易理解,但是能夠解決大部分的問題。但是卻存在著致命的缺點。可以想象,如果需要在一段代碼中調用多次sleep()
函數,將會出現下面的情況:
var i = 0;//記錄sleep()函數調用的次數
function sleep(ms, callback){
setTimeout(function(){
if(i < 2){
i++;
callback("finish", null);
}else{
callback(null, new Error('i大于2'));
}
}, ms);
}
//第一次調用
sleep(1000, function (val, err) {
if (err) console.log(err.message);
else {
console.log(val);
//第二次調用
sleep(1000, function (val, err) {
if (err) console.log(err.message);
else {
console.log(val);
//第三次調用
sleep(1000, function (val, err) {
if (err) console.log(err.message);
else {
console.log(val);
}
});
}
});
}
});
//輸出結果分別為:finish,finish,i大于2。
由上面的例子可以看出,假如需要多次調用sleep()
函數時,就會進行多次的嵌套。這種嵌套雖然可以使用,但是在代碼的閱讀、維護和美觀上面,都是反人類出現的。所以,在新標準ES6和ES7發布之后,將會逐漸摒棄這種寫法。
事件監聽形式的異步控制
對于callback
的改進就是使用事件監聽的形式進行操作:每次調用異步函數都會返回一個EvetEmitter
對象。在函數內部,可以根據需求來觸發不同的事件。對不同的事件進行監聽,然后做出相應的處理。具體代碼如下:
var i = 0;
var events = require('events');
var emitter = new events.EventEmitter();//創建事件監聽器的一個對象
function sleep(ms) {
setTimeout(function () {
i++;
if (i < 2) {
emitter.emit('done', 'finish');//觸發事件'done'
} else {
emitter.emit('error', new Error('i大于2'));//觸發事件'error'
}
}, ms);
}
var emit = sleep(1000);
//監聽事件'done'
emitter.on('done', function(val){
console.log(val);
});
//監聽事件'error'
emitter.on('error', function(val){
console.log(val);
});
通過事件監聽的形式進行異步處理,可以更加直觀和多樣性。但是和callback
類似,并沒有解決嵌套的問題。例如需要調用多次sleep()
函數,依然需要在函數emitter.on('done', function(val))
中進行嵌套。
Promise對象進行異步控制處理
比較Promise和EventEmitter,可以發現兩個十分明顯的區別。對于EventEmitter,我們可以根據自己的需求來定義多種監聽事件,然后對不同的事件進行不同的處理;對于promise,則可以對將異步操作的結果進行返回,然后根據返回值來進行相應的操作。對于promise的詳細學習,可以通過在MDN查閱詳細且最新的資料。
代碼如下:
let num = 1
function sleep(ms) {
//返回一個Promise對象
return new Promise(function (resolev, reject) {
setTimeout(function () {
if (num < 1) {
num++
//如果成功,對resolve處理
resolev(new Error('finish'))
} else {
//如果失敗,對reject操作
reject('i大于2')
}
}, ms)
})
}
sleep(300).then(value => {
console.log(value)
return sleep(300)
}).then(value => {
console.log(value)
return sleep(300)
}).then(value => {
console.log(value)
return sleep(300)
}).catch(function (error) {
console.log(error)
})
和callback與EventEmmitter相比,Promise將異步處理的嵌套函數進行了展開,更利于閱讀和理解。當promise鏈中的任意一個函數出錯都會直接拋出到鏈的最底部,所以我們統一用了一個catch
去捕獲,每次promise的回調返回一個promise,這個promise把下一個then
當作自己的回調函數,并在resolve
之后執行,或在reject
后被catch
出來。
async/await解決異步控制
雖然Promise將嵌套函數進行了展開,但是依然是鏈式函數,并沒有解決回調本身。因此在最新的ES7中,引入了async/await
函數,來解決這個問題。async
函數返回一個 Promise 對象,可以使用then
方法添加回調函數。當函數執行的時候,一旦遇到await
就會先返回,等到異步操作完成,再接著執行函數體內后面的語句。
代碼如下:
let num = 1
function sleep(ms) {
//返回一個Promise對象
return new Promise(function (resolev, reject) {
setTimeout(function () {
if (num < 3) {
num++
//如果成功,對resolve處理
resolev('finish')
} else {
//如果失敗,對reject操作
reject(new Error('i大于2'))
}
}, ms)
})
}
//用關鍵詞async修飾函數
async function asyncSleep() {
try {
let value
//用await來修飾函數
value = await sleep(300)
console.log(value)
value = await sleep(300)
console.log(value)
value = await sleep(300)
console.log(value)
} catch (error) {
console.log(error)
}
}
//調用函數
asyncSleep()
在上面的函數中,我們進行逐步分析
- 我們首先將
setTimeout
函數進行封裝,讓其返回Promise對象。 - 然后聲明一個用
async
修飾的函數,在里面調用sleep()
函數。 - 對于每次
sleep()
函數,我們用await關鍵詞進行修飾,表示下面的操作需要進行異步處理。 - 聲明一個value變量來接受
sleep()
函數返回的Promise對象。 - 最后用catch來對異常進行處理。
總的來說async/await
是promise的語法糖,但它能將原本異步的代碼寫成同步的形式,try...catch
也是比較友好的捕獲異常的方式所以在今后寫node的時候盡量多用promise或者async/await
,對于回調就不要使用了,大量嵌套真的很反人類。
參考資料
node.js異步控制流程 回調,事件,promise和async/await
ECMAScript 6 入門, 阮一峰