node.js異步控制流程 回調,事件,promise和async/await

寫這個問題是由於最近看到一些初學者用回調用的不亦樂乎,最後代碼左調來又調去很不直觀。node

首先上結論:推薦使用async/await或者co/yield,其次是promise,再次是事件,回調不要使用。promise

接下來是解析,爲何我會有這樣的結論緩存

 

首先是回調,理解上最簡單,就是我把任務分配出去,當你執行完了我就能從你那裏拿到結果執行相應的回調,異步

這裏演示一個對setTimeout的封裝,規定時間後打印相應結果並執行回調函數async

而且這個函數傳給回調函數的參數符合node標準,第一個爲error信息,若是出錯error不爲null,正常執行則爲null函數

var i = 0;
function sleep(ms, callback) {
    setTimeout(function () {
        console.log('我執行完啦!');
        i++;
        if (i >= 2) callback(new Error('i大於2'), null);
        else callback(null, i);
    }, ms);
}

sleep(3000, function (err,val) {
    if(err) console.log('出錯啦:'+err.message);
    else console.log(val);
})

//執行結果:3s後打印 "我執行完啦","1"

 

 

這樣的代碼看上去並不會很不舒服,並且也比較好理解,可是假如我要暫停屢次呢ui

調用的代碼就變成了以下:spa

sleep(1000, function (err, val) {
    if (err) return console.log(err.message);;
    console.log(val);
    sleep(1000, function (err, val) {
        if (err) return console.log(err.message);
        console.log(val);
        sleep(1000, function (err, val) {
            if (err) console.log(err.message);
            else console.log(val);
        })
    })
})

能夠看得出來,嵌套得很深,你能夠把這三次操做當作三個異步任務,而且還有可能繼續嵌套下去,這樣的寫法顯然是反人類的。code

嵌套得深首先一個不美觀看的很不舒服,第二個若是回調函數出錯了也難以判斷在哪裏出錯的。對象

 

因而改進方法就是事件監聽,每次調用一個異步函數都返回一個EventEmitter對象,並在執行成功時調用done事件,

失敗時調用error事件

var i = 0;
function sleep(ms) {
    var emitter = new require('events')();
    setTimeout(function () {
        console.log('我執行完啦!');
        i++;
        if (i >= 2) emitter.emit('error', new Error('i大於2'));
        else emitter.emit('done', i);
    }, ms);
}

var emit = sleep(3000);
emit.on('done',function (val) {
    console.log('成功:' + val);
})
emit.on('error',function(err){
    console.log('出錯了:' + err.message);
})

這樣寫比以前的好處在於能添加多個回調函數,每一個回調函數都能得到值並進行相應操做。但這並無解決回調嵌套的問題,

好比這個函數屢次調用仍是必須寫在ondone的回調函數裏,看起來仍是很不方便。

因此比較廣泛的解決方案是Promise。

promise和事件相似,你能夠把它當作只觸發兩個事件的event對象,可是事件具備即時性,觸發以後這個狀態就不存在了,這個

事件已經觸發過了,你就再也拿不到值了,而promise不一樣,promise只有兩個狀態resolve和reject,當它觸發任何一個狀態後

它會將當前的值緩存起來,並在有回調函數添加進來的時候嘗試調用回調函數,若是這個時候尚未觸發resolve或者reject,那麼

回調函數會被緩存,等待調用,若是已經有了狀態(resolve或者reject),則馬上調用回調函數。而且全部回調函數在執行後都當即

被銷燬。

代碼以下:

var i = 0;
//函數返回promise
function sleep(ms) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log('我執行好了');
            i++;
            if (i >= 2) reject(new Error('i>=2'));
            else resolve(i);
        }, ms);
    })
}

sleep(1000).then(function (val) {
    console.log(val);
    return sleep(1000)
}).then(function (val) {
    console.log(val);
    return sleep(1000)
}).then(function (val) {
    console.log(val);
    return sleep(1000)
}).catch(function (err) {
    console.log('出錯啦:' + err.message);
})

這個例子中,首先它將本來嵌套的回調函數展開了,如今看的更舒服了,而且因爲promise的冒泡性質,當promise鏈中的任意一個

函數出錯都會直接拋出到鏈的最底部,因此咱們統一用了一個catch去捕獲,每次promise的回調返回一個promise,這個promise

把下一個then看成本身的回調函數,並在resolve以後執行,或在reject後被catch出來。這種鏈式的寫法讓函數的流程比較清楚了,

拋棄了嵌套,終於能平整的寫代碼了。

但promise只是解決了回調嵌套的問題,並無解決回調自己,咱們看到的代碼依然是用回調阻止的。因而這裏就引入了async/await

關鍵字。

 

async/await是es7的新標準,而且在node7.0中已經獲得支持,只是須要使用harmony模式去運行。

async函數定義以下

async function fn(){
    return 0;
}

即便用async關鍵字修飾function便可,async函數的特徵在於調用return返回的並非一個普通的值,而是一個Promise對象,若是

正常return了,則返回Promise.resolve(返回值),若是throw一個異常了,則返回Promise.reject(異常)。也就是說async函數的返回

值必定是一個promise,只是你寫出來是一個普通的值,這僅僅是一個語法糖。

await關鍵字只能在async函數中才能使用,也就是說你不能在任意地方使用await。await關鍵字後跟一個promise對象,函數執行到await後會退出該函數,直到事件輪詢檢查到Promise有了狀態resolve或reject 才從新執行這個函數後面的內容。

首先我用剛剛的例子展現async/await的神奇之處

var i = 0;
//函數返回promise
function sleep(ms) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log('我執行好了');
            i++;
            if (i >= 2) reject(new Error('i>=2'));
            else resolve(i);
        }, ms);
    })
}

(async function () {
    try {
        var val;
        val = await sleep(1000);
        console.log(val);
        val = await sleep(1000);
        console.log(val);
        val = await sleep(1000);
        console.log(val);
    }
    catch (err) {
        console.log('出錯啦:'+err.message);
    }
} ())

 

看上去代碼是徹底同步的,每等待1s後輸出一次,而且在sleep返回的promise中狀態爲reject的時候還能被try...catch出來。

那麼這究竟是怎麼回事呢 咱們來看一張圖

這段代碼和剛剛的代碼同樣,只是在async函數被調用後輸出了一次"主程序沒有被調用",結果以下

咱們發現後面輸出的話是先打印的,這好像和咱們的代碼順不同,這是怎麼回事呢。

 

 

總的來講async/await是promise的語法糖,但它能將本來異步的代碼寫成同步的形式,try...catch也是比較友好的捕獲異常的方式

因此在從此寫node的時候儘可能多用promise或者async/await,對於回調就不要使用了,大量嵌套真的很反人類。

相關文章
相關標籤/搜索