從「async」到async——Node異步流程控制總結

Node的異步概念

理解異步非阻塞

提到Node,異步非阻塞會是第一個須要你理解的概念。不少人會把這其實是兩個概念的詞混爲一談,認爲異步就是非阻塞的,而同步就是阻塞的。從實際的效果出發,異步IO和非阻塞IO實際上都能達到咱們對於IO繁重的網絡應用並行IO的追求。可是實際上這是兩個很不同的概念。html

從操做系統的內核角度出發,I/O調用只有兩種方式,阻塞和非阻塞。兩者的區別在於,對於使用阻塞IO調用,應用程序須要等待IO的整個過程都所有完成,即完成整個IO目的,此期間CPU進行等待,沒法獲得充分的利用。而對於使用非阻塞IO調用來講,應用程序發起IO請求以後不等待數據就當即返回,接下來的CPU時間片可用於其餘任務,因爲整個IO的過程並無完成,因此還須要使用輪詢技術去試探數據是否完整準備好。關於輪詢技術細節和發展,此處不過多贅述,很推薦樸靈老師《深刻淺出NodeJs》的第三章。前端

不難理解,從應用程序的角度出發,我無論你操做系統內核是阻塞的IO調用仍是非阻塞的IO調用,只要是我要的數據並無給我,那麼這就是同步的,由於我依舊是在等數據。因此對於這種狀況下,應用程序的那「一根筋」就能夠選擇用同步仍是異步的方式去面對該狀況。同步即等待操做系統給到數據再進行下面的代碼(單線程),異步即發出請求以後也當即返回,用某一種方式註冊未完成的任務(回調函數)而後繼續往下執行代碼。node

理解進程,線程,協程

爲了使多個程序可以併發(同一時刻只有一個在運行,時間維度稍微拉長,就會感受起來像多個同時運行)便有了這個在操做系統中可以獨立運行並做爲資源分配的基本單位git

進程是資源分配的基本單位,進程的調度涉及到的內容比較多(存儲空間,CPU,I/O資源等,進程現場保護),調度開銷較大,在併發的切換過程效率較低。爲了更高效的進行調度,提出了比進程更輕量的獨立運行和調度的基本單位線程。最主要的一點同一個進程的多個線程共享進程的資源,這就會暴露出一個多線程編程中須要加入多線程的鎖機制來控制資源的互斥性(同時寫變量衝突)。線程調度能大幅度減少調度的成本(相對於進程來講),線程的切換不會引發進程的切換,可是畢竟仍是有成本。es6

面對着線程相關的問題,出現了協程。協程是用戶模式下的輕量級線程操做系統內核對協程一無所知,協程的調度徹底有應用程序來控制,操做系統無論這部分的調度。github

協程的特色在因而一個線程執行,所以最大的優點就是協程極高的執行效率。由於子程序切換不是線程切換,而是由程序自身控制,所以,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優點就越明顯。第二大優點就是不須要多線程的鎖機制,由於只有一個線程,就也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只須要判斷狀態就行了,因此執行效率比多線程高不少。shell

依據上述概念自己咱們可能能夠得出一種暫時性的結論:考慮到利用多核CPU,而且充分發揮協程的高效率,又可得到極高的性能,面向開發人員最簡單的方法是多進程+協程,既充分利用多核數據庫

在Node中利用多核CPU的子進程文檔npm

回調函數問題

在Node中每個異步的IO回調函數並非由開發人員所控制主動執行的。編程

那麼對於Node的異步IO,在咱們最常使用的異步回調的形式下,咱們發出調用到回調函數執行這中間發生了什麼?

整個過程可簡單的抽象成四個基本要素:IO線程池觀察者請求對象,以及事件循環,盜用《深刻淺出NodeJS》的Windows借用IOCP實現異步回調過程的一張圖片:

clipboard.png

其中所要執行的異步回調函數以及相關的全部狀態參數會被封裝成一個請求對象而後被推入到IO線程池中,當操做系統執行完IO獲得結果以後會將數據放入請求對象中,並歸還當前線程至線程池,通知IOCP完成了IO過程,而後事件循環IO觀察者中獲得已經能夠執行的請求對象中的回調,灌注IO數據結果開始執行。

Node自己是多線程的,開發人員的JS代碼單線程化身爲一個老闆,實現高效的異步邏輯依靠的是Node機制內部的各個線程池,模擬出了一個異步非阻塞的特色。呈如今開發人員面前的是表現形式爲各類各樣的callback組成的一個原生編程風格

異步編程與「回調地獄」

const fs = require('fs')

fs.readFile("./test1.txt", "utf-8", function(err,content1){
    if (err) {
        console.log(err)
    } else {
        fs.readFile(content1, "utf-8", function(err,content2){
            if (err) {
                console.log(err);
            } else {
                fs.readFile(content2, "utf-8", function(err,content3){
                    if (err) {
                        console.log(err);
                    } else {
                        console.log(content3)
                    }
                });
            }
        });
    }
});

console.log('主線程')


try {
    console.log(content3)
} catch(e) {
    console.log("尚未獲取到content3!");
}

讀取的每個 .txt 文件中的內容是要讀取的下一個文件的路徑地址,最後一個txt文件(test3.txt)中的內容是「callback hell is not finished......」

打印結果:

主線程
尚未獲取到content3!
callback hell is not finished......

能夠理解爲Node代碼一根筋的往下想盡快結束所謂的主線程,因此遇到設計異步的就自動忽略並跳過爲了往下執行,因此出現了第一句非異步的打印操做,打印「主線程」,再往下執行遇到須要打印 content3 這個變量的時候,主線程就「懵」了,由於命名空間內並無獲取到任何 content3 的數據,甚至在主線程命名空間內都沒有定義這個變量,若是不用 try-catch 那麼應該會報 「content3 is not defined」的錯誤。

此外,callback hell 盡收眼底,一味地由於依賴而採用嵌套回調函數的方式,哪怕是上述代碼那麼簡單的一個原子性的操做都會被這種「橫向發展」的代碼和無休止的大括號嵌套讓業務邏輯代碼喪失掉可維護性和可讀性。

爲了不這種回調地獄,解決問題的方案和第三方模塊就開始層出不窮百花齊放了。

這個async不是ES2017的async

async是一個十分強大,功能十分全面提供異步編程解決法案的一個第三方npm模塊。也是我所接觸的公司中的項目中大範圍使用的。下面是關於這個模塊的經常使用函數使用介紹,先感覺一下。

流程控制函數

  • async.parallel(tasks,callback)

    • tasks 能夠是一個數組也能夠是個對象,他的數組元素值或者對象的屬性值就是一個一個異步的方法。

parallel方法用於並行執行多個方法,全部傳入的方法都是當即執行,方法之間沒有數據傳遞。傳遞給最終callback的數組中的數據按照tasks中聲明的順序,而不是執行完成的順序

//以數組形式傳入須要執行的多個方法
async.parallel([
    function(callback){//每一個function均須要傳入一個錯誤優先的callback
        // 異步函數1,好比 fs.readFile(path,callback)
    },
    function(callback){
        // 異步函數2
    }
],
//最終回調 
function(err, results){
    // 當tasks中的任一方法發生錯誤,即回調形式爲callback('錯誤信息')時,錯誤將被傳遞給err參數,未發生錯誤err參數爲空
    if(err){
        console.log(err)
    }else{
        let one = results[0];
        let two = results[1];
        //你的各類操做
    }
    // results中爲數組中,兩個方法的結果數組:[異步1的結果, 異步2的結果] ,即便第二個方法先執行完成,其結果也是在第一個方法結果以後
});
 
//以object對象形式傳入須要執行的多個方法
async.parallel({
    one: function(callback){
        // 異步函數1
    },
    two: function(callback){
        // 異步函數2
    }
},
function(err, results) {
    // 當tasks中的任一方法發生錯誤,即回調形式爲callback('錯誤信息')時,錯誤將被傳遞給err參數,未發生錯誤err參數爲空
    // // results 如今等於: {one: 異步1的結果, two: 異步2的結果}
});
  • 使用時所要注意的事項:

    • 當tasks中的任一方法發生錯誤時,錯誤將被傳遞給最終回調函數的err參數,未發生錯誤err參數爲空。
    • tasks用數組的寫法,即便第二個方法先執行完成,其結果也是在第一個方法結果以後,兩個方法的結果數組:[異步1的結果, 異步2的結果]

我的感覺:這個方法的大量使用讓我以爲當一個要展現不少方面的信息的首頁時,解耦成了代碼可讀性的最關鍵因素,親身體會的是使用這個方法在企業業務邏輯中理想狀況是在 tasks 中註冊的並行任務獲得的結果最好可以直接使用,而不是在第一個async.parallel的最終回調中依舊須要依賴獲得的結果再進行下個系列的異步操做,由於這樣致使的結果直接就變成了代碼繼續向着橫向發展,比原生的 callback hell 並無要好到哪裏去。篇幅緣由就不展現實際代碼了,總之雖然結果流程獲得了一個較爲明確的控制,可是依舊沒有良好的可讀性

  • async.series(tasks,callback)

series方法用於依次執行多個方法,一個方法執行完畢後纔會進入下一方法,方法之間沒有數據傳遞!!

參數和形式與上面的 async.parallel(tasks,callback)一致

//以數組形式傳入須要執行的多個方法
async.series([
    function(callback){
       fs.readFile(path1,callback)
    },
    function(callback){
       fs.readFile(path2,callback)
    }
],
// 可選的最終回調 
function(err, results){
    // 當tasks中的任一方法發生錯誤,即回調形式爲callback('錯誤信息')時,錯誤將被傳遞給err參數,未發生錯誤err參數爲空
    // results中爲數組中兩個方法的結果數組:['one', 'two'] 
});

這個方法在 tasks 中註冊的異步函數之間雖然沒有數據傳遞,可是這個方法控制了這些個異步方法的執行順序,而且只要一個函數執行失敗了接下來的函數就不會再執行了,而且把 err 傳遞到最終的回調函數中的 err 參數中。正如它的名字 「series」所說,這個方法有點數據庫中的事務控制的意思,只不過原生不支持回滾罷了。

  • async.waterfall(tasks,callback)

waterfall方法與series方法相似用於依次執行多個方法,一個方法執行完畢後纔會進入下一方法,不一樣與series方法的是,waterfall之間有數據傳遞,前一個函數的輸出爲後一個函數的輸入。waterfall的多個方法只能以數組形式傳入,不支持object對象。

async.waterfall([
    function(callback) {
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback) {
        // arg1 如今是 'one', arg2 如今是 'two' 
        callback(null, 'three');
    },
    function(arg1, callback) {
        // arg1 如今是 'three' 
        callback(null, 'done');
    }
], function (err, result) {
    //執行的任務中方法回調err參數時,將被傳遞至本方法的err參數
    // 參數result爲最後一個方法的回調結果'done'     
});

由於 tasks 中註冊的異步函數數組中前一個函數的輸出做爲後一個輸入,很天然的就能夠想到能夠經過前一個函數傳遞「處理成功信號」在第二個函數中進行判斷來進行一系列完整的簡單相似於事務控制的邏輯操做。

  • async.auto(tasks,callback)

auto方法根據傳入的任務類型選擇最佳的執行方式。不依賴於其它任務的方法將併發執行,依賴於其它任務的方法將在其執行完成後執行。相似於「依賴注入」概念。

async.auto({
    getData: function(callback){
         //一個取數據的方法
        // 與makeFolder方法並行執行
        callback(null, 'data', 'converted to array');
    },
    makeFolder: function(callback){
        // 一個建立文件夾的方法
        // 與make_folder方法並行執行
        callback(null, 'folder');
    },
    writeFile: ['getData', 'makeFolder', function(callback, results){
        // 此方法在等待getData方法和makeFolder執行完成後執行,而且在results中拿到依賴函數的數據
        callback(null, 'filename');
    }],
    sendEmail: ['writeFile', function(callback, results){
        // 等待writeFile執行完成後執行,results中拿到依賴項的數據
        callback(null, {'file':results.writeFile, 'email':'user@example.com'});
    }]
}, function(err, results) {
    console.log('err = ', err);
    console.log('results = ', results);
});

我的評價:喜歡這種方法,有清晰的可讀性,依賴規則以及控制一目瞭然,很惋惜的是在咱們的代碼裏面並無使用。缺點是相比較咱們的最終解決方案的優雅,這個仍是會有可能嵌套不少層的大括號的方式有它自己的劣勢。

異步集合操做

  • async.each(arr,iterator(item, callback),callback)

對數組arr中的每一項執行iterator操做。iterator方法中會傳一個當前執行的項及一個回調方法。each方法中全部對象是並行執行的。對數組中每一項進行 iterator 函數處理,若是有一項出錯則最終的回調的 err 就回事該 err。可是,出錯並不會影響到其餘的數組元素執行。

const async = require('async')
const fs = require('fs')
let arr = ['./Test/file1.txt',"./Test/file2.txt","./Test/file3.txt"]
let iterator = (item,callback)=>{   
        fs.readFile(item,"utf-8",(err,results)=>{
            if(item === "./Test/file2.txt"){
                callback(new Error('wrong'))
            }else{
                console.log(results);
                callback(null,results)
            }          
        })      
}
async.each(arr,iterator,function(err){
    if(err){
        console.log(err)
    }
})

打印結果:

3
Error: wrong
    at fs.readFile (/Users/liulei/Desktop/asyncEach/test.js:10:26)
    at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:511:3)
1

可見,因爲併發的緣由,便是第二項出錯,也不會影響其他的元素執行。若是想要讓數組中的元素按照順序執行,而且一旦一個出錯,後面的數組元素都將不會執行的狀況應該用另外一個函數 async.eachSeeries(arr,iterator(item, callback),callback),用法什麼的都同樣,這裏就不贅述了。

此外,each方法的最終回調函數能夠看出來的是,並不會被傳入任何結果,因此最終的回調函數就只有一個參數那就是 err,若是想要向最終回調函數中傳入某些結果那麼還要用到接下來介紹的 asycnc.map()

  • async.map(arr,iterator(item, callback),callback)

map方法使用方式和each徹底同樣,與each方法不一樣的是,map方法用於操做對象的轉換,轉換後的新的結果集會被傳遞至最終回調方法中(不出錯的狀況下)呈現一個新的數組的形似。

一樣的是,map也是並行操做,如需按順序而且出錯就中止則須要使用 async.mapSeries

向Promise的過渡

Promise基礎簡要介紹

一個簡單清晰的例子:

const fs = require('fs')

fs.readFile("./Test/file1.txt", "utf-8", (err, content) => {
    if (err) {
        console.log(err);
    } else {
        console.log(content);
    }
})

let readFile = () => {
    return new Promise((resolve, reject) => {
        fs.readFile("./Test/file2.txt", "utf-8", (err, content) => {
            if (err) {
                reject(err)
            } else {
                resolve(content);
            }
        })
    })
}

readFile()
    .then((res) => {
        console.log(res);
    })
    .catch((err) => {
        console.log(err);
    })

只是比原生的callback形式的異步函數多了一步封裝包裹的過程。Promise是一個對象,能夠把它看作是一個包含着異步函數可能出現的結果(成功或者失敗(err))的「異步狀態小球」。獲得了這個小球你就能用 then 去弄他,用 catch 去捕獲它的失敗。簡單的歸納,也僅此而已。基於這個小球,咱們就能獲得所謂的「現代異步處理方案」了,後話。

前端 Promisify Ajax請求:

let btn = document.getElementById("btn")
let getData = (api) => {
    return new Promise((resolve,reject)=>{
        let req = new XMLHttpRequest();
        req.open("GET",api,true)       
        req.onload = () => {
              if (req.status === 200) {
                resolve(req.responseText)
              } else {
                reject(new Error(req.statusText))
              }
            }
        
        req.onerror = () => {
              reject(new Error(req.statusText))
            }
            req.send()
          })
        }

btn.onclick = function(e) {
    getData('/api')
        .then((res) => {
            let content=JSON.parse(res).msg
            document.getElementById("content").innerText = content
            })
        .catch((err) => {
            console.log(err);
            })
        }

Node提供的原生模塊的API基本上都是基於一個 callback 形式的函數,咱們想用 Promise ,難不成甚至原生的這些最原始的函數都要咱們手動去進行 return 一個 Promise 對象的改造?其實不是這樣的,Node 風格的 callback 都聽從着「錯誤優先」的回到函數方案,即形如(err,res)=>{},而且回調函數都是最後一個參數,他們的形式都是一致的。因此Node的原生util模塊提供了一個方便咱們將函數 Promisfy 的工具——util.promisfy(origin)

let readFileSeccond = util.promisify(fs.readFile)

readFileSeccond("./Test/file3.txt","utf-8")
    .then((res) => {
        console.log(res);
    })
    .catch((err) => {
        console.log(err);
    })

注意,這個原生工具會對原生回調的結果進行封裝,若是在最後的回調函數中除了 err 參數以外,還有不止一個結果的狀況,那麼 util.promisify 會將結果都統一封裝進一個對象之中。

用Promise提供方法應對不一樣的狀況

實際代碼邏輯中咱們可能會面對各類異步流程控制的狀況,像是以前介紹 async 模塊同樣,一種很常見的狀況就是有不少的異步方法是能夠同時併發發起請求的,即互相不依賴對方的結果,async.parallel的效果那樣。Promise 除了封裝異步以外還未咱們提供了一些原生方法去面對相似這樣的狀況:

知識準備

  • Promise.resolve(value)

它是下面這段代碼的語法糖:

new Promise((resolve)=>{
    resolve(value)
})

注意點,在 then 調用的時候即使一個promise對象是當即進入完成狀態的,那Promise的 then 調用也是異步的,這是爲了不同步和異步之間狀態出現了模糊。因此你能夠認爲,Promise 只能是異步的,用接下的代碼說明:

let promiseA = new Promise((resolve) => {
    console.log("1.構造Promise函數");
    resolve("ray is handsome")
})

promiseA.then((res) => {
    console.log("2.成功態");
    console.log(res);
})

console.log("3.最後書寫");

上面的代碼,打印的結果以下:

1.構造Promise函數
3.最後書寫
2.成功態
ray is handsome

promise 能夠鏈式 then ,每個 then 以後都會產生一個新的 promise 對象,在 then 鏈中前一個 then 這種能夠經過 return的方式想下一個 then 傳遞值,這個值會自動調用 promise.resolve()轉化成一個promise對象,代碼說明吧:

const fs = require('fs')
let promise = Promise.resolve(1)
promise
    .then((value) => {
            console.log(value)
            return value+1
    })
    .then((value) => {
            console.log(`first那裏傳下來的${value}`);
            return value+1
    })
    .then((value) => {
            console.log(`second那裏傳下來的${value}`);
            console.log(value)
    })
    .catch((err) => {
        console.log(err);
    })

上面的代碼答應的結果:

1
first那裏傳下來的2
second那裏傳下來的3
3

此外 then 鏈中應該添加 catch 捕獲異常,某一個 then 中出現了錯誤則執行鏈會跳事後來的 then 直接進入 catch

獲得 async.parallel一樣的效果

Promise 提供了一個原生方法 Promise.all(arr),其中arr是一個由 promise 對象組成的一個數組。該方法能夠實現讓傳入該方法的數組中的 promise 同時執行,並在全部的 promise 都有了最終的狀態以後,纔會調用接下來的 then 方法,而且獲得的結果和在數組中註冊的結果保持一致。看下面的代碼:

const fs = require('fs')
const util = require('util')

let readFile = util.promisify(fs.readFile)

let files = [readFile("../../Test/file1.txt","utf-8"),
            readFile("../../Test/file2.txt","utf-8"),
            readFile("../../Test/file3.txt","utf-8"),]

Promise.all(files)
    .then((res) => {
        console.log(res)
    })
    .catch((err) => {
        console.log(err);
    })

上面的代碼最終會打印,便是按順序的三個txt文件裏面的內容組成的數組:

[‘1’,‘2’,‘3’]

對比 async.parallel的用法,發現獲得相同的結果。

此外,與 Promise.all方法相對應的還有一個Promise.race,該方法與all用法相同,一樣是傳入一個由 promise 對象組成的數組,你能夠把上面的代碼中的 all 直接換成 race 看看是什麼效果。沒錯,對於指導 race 這個英文單詞意思的可能已經猜出來了,race 競爭,賽跑,就是隻要數組中有一個 promise 到達最終態,該方法的 then 就會執行。因此該代碼有可能會出現'1','2','3'中的任何一個字符串。

至此,咱們解決了要改造的代碼的第一個問題,那就是多異步的同時執行,那麼以前 async 模塊介紹的其餘的的功能在實際運用中也很常見的幾個場景,相似順序執行異步函數,異步集合操做要怎麼使用新的方案模擬出來呢?真正的原生 async要登場了。

所謂的異步流程控制的「終極解決方案」————async

在開始介紹 async 以前,想先聊一種狀況。

基於 Promise 的這一套看似可讓代碼「豎着寫」,能夠很好的解決「callbackHell」回調地獄的窘境,可是上述全部的例子都是簡單場景下。在基於 Promise 的 then 鏈中咱們不難發現,雖然一層層往下的 then 鏈能夠向下一層傳遞本層處理好的數據,可是這種鏈條並不能跨層使用數據,就是說若是第3層的 then 想直接使用第一層的結果必須有一個前提就是第二層不只將本身處理好的數據 return 給第三層,同時還要把第一層傳下來的再一次傳給第三層使用。否則還有一種方式,那就是咱們從回調地獄陷入另外一種地獄 「Promise地獄」。

借用這篇博客 的一個操做 mongoDB 場景例子說明:

MongoClient.connect(url + db_name).then(db => {
    return db.collection('blogs');
}).then(coll => {
    return coll.find().toArray();
}).then(blogs => {
    console.log(blogs.length);
}).catch(err => {
    console.log(err);
})

若是我想要在最後一個 then 中獲得 db 對象用來執行 db.close()關閉數據庫操做,我只能選擇讓每一層都傳遞這個 db 對象直至我使用操做 then 的盡頭,像下面這樣:

MongoClient.connect(url + db_name).then(db => {
    return {db:db,coll:db.collection('blogs')};
}).then(result => {
    return {db:result.db,blogs:result.coll.find().toArray()};
}).then(result => {
    return result.blogs.then(blogs => {   //注意這裏,result.coll.find().toArray()返回的是一個Promise,所以這裏須要再解析一層
        return {db:result.db,blogs:blogs}
    })
}).then(result => {
    console.log(result.blogs.length);
    result.db.close();
}).catch(err => {
    console.log(err);
});

下面陷入 「Promise地獄」:

MongoClient.connect(url + db_name).then(db => {
    let coll = db.collection('blogs');
    coll.find().toArray().then(blogs => {
        console.log(blogs.length);
        db.close();
    }).catch(err => {
        console.log(err);
    });
}).catch(err => {
    console.log(err);
})

看上去不是那麼明顯,可是已經出現了 then 裏面嵌套 then 了,操做一多直接一晚上回到解放前,再一次喪失了讓人想看代碼的慾望。OK,用傳說中的 async 呢

(async function(){
    let db = await MongoClient.connect(url + db_name);
    let coll = db.collection('blogs');
    let blogs = await coll.find().toArray();
    console.log(blogs.length);
    db.close();
})().catch(err => {
    console.log(err);
});

各類異步寫的像同步了,async(異步)關鍵字聲明,告訴讀代碼的這是一個包含了各類異步操做的函數,await(得等它)關鍵字說明後面的是個異步操做,卡死了等他執行完再往下。這個語義以及視覺確實無法否定這多是「最好的」異步解決方案了吧。

不得不提的 co 模塊

衆所周知的是 async 函數式 generator 的語法糖,generator 在異步流程控制中的執行依賴於執行器,co 模塊就是一個 generator 的執行器,在真正介紹和使用 async 解決法案以前有必要簡單瞭解一下大名鼎鼎的 co 模塊。

什麼是 generator,詳細請參考Ecmascript6 入門

var fs = require('fs');

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) reject(error);
      resolve(data);
    });
  });
};

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
// 執行生成器,返回一個生成器內部的指針
var g = gen();
//手動 generator 執行器
g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
})

上述代碼採用 generator 的方式在 yeild 關鍵字後面封裝了異步操做並經過 next()去手動執行它。調用 g.next() 是去執行 yield 後面的異步,這個方案就是經典的異步的「協程」(多個線程互相協做,完成異步任務)處理方案。

協程執行步驟:

  1. 協程A開始執行。
  2. 協程A執行到一半,進入暫停,執行權轉移到協程B。
  3. (一段時間後)協程B交還執行權。
  4. 協程A恢復執行。

協程遇到 yield 命令就暫停 等到執行權返回,再從暫停的地方繼續日後執行。

翻譯上述代碼:

  • gen()執行後返回一個生成器的內部執行指針,gen 生成器就是一個協程。
  • gen.next()讓生成器內部開始執行代碼到遇到 yield 執行 yield 後,就暫停該協程,而且交出執行權,此時執行權落到了JS主線程的手裏,即開始執行 Promise 的 then 解析。
  • then 的回調裏取得了該異步數據結果,調用g.next(data)經過網next()函數傳參的形式,將結果返回給生成器的f1變量。
  • 依次回調類推。

說明:

  • g.next()返回一個對象,形如{ value: 一個Promise, done: false }到生成器內部代碼執行完畢返回{ value: undefined, done: true }

引出一個問題: 咱們不能每一次用 generator 處理異步都要手寫 generator 的 then 回調執行器,該格式相同,每次都是調用.next(),因此能夠用遞歸函數封裝成一個函數:

function run(gen){
  var g = gen();

  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }

  next();
}

run(gen);

上述執行器的函數編寫 co 模塊考慮周全的寫好了,co模塊源碼

你只須要:

const co = require('co')
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res); 
}).catch(onerror);

yield 後面的是併發。

此時咱們來對比 async 寫法:)

async function(){
    var res = await [
    Promise.resolve(1),
    Promise.resolve(2)
    ]
    console.log(res);
}().catch(onerror);

async 函數就是將 Generator 函數的星號(*)替換成 async,將 yield 替換成 await,僅此而已。而且它不須要額外的執行器,由於它自帶 Generator 執行器

本質上其實並無脫離「協程」異步的處理方式

const fs = require('fs')
const util = require('util')


let readFile = util.promisify(fs.readFile);

(async function fn() {
    var a = await readFile('./test1.txt',"utf-8")
    var b = await readFile('./test2.txt',"utf-8")
    console.log(a)
    console.log(b)
})()
.catch((e)=>{
    console.log("出錯了")
})



console.log('主線程')

打印結果會先輸出「主線程」。

async 解決方案

前文咱們經過 Promise.all()解決了 async.paralle()的功能,如今咱們來看看用 Promise 配合原生 async 來達到「async」模塊的其餘功能。

  • 實現 async.series 順序執行異步函數
//源代碼
async.series([
        function(callback) {
            if (version.other_parameters != otherParams) { // 更新其餘參數
                var newVersion = {
                    id: version.id,
                    other_parameters: otherParams,
                };
                CVersion.update(newVersion, callback);
            } else {
                callback(null, null);
            }
        },
        function(callback) {
            cVersionModel.removeParams(version.id, toBeRemovedParams, callback);
        },
        function(callback) {
            cVersionModel.addParams(version.id, toBeAddedParams, callback);
        },
        function(callback) {
            CVersion.get(version.id, callback);
        },
    ], function(err, results) {
        if (err) {
            logger.error("更新電路圖參數失敗!");
            logger.error(version);
            logger.error(tagNames);
            logger.error(err);
            callback(err);
        } else {
            callback(null, results[3].parameters);
        }
    });


//新代碼

(async function(){
    if (version.other_parameters != otherParams) { // 更新其參數
        var newVersion = {
            id: version.id,
            other_parameters: otherParams,
        };
        await  CVersion.update(newVersion);
    } else {
        return null
    }
    await cVersionModel.removeParams(version.id, toBeRemovedParams)
    await cVersionModel.addParams(version.id, toBeAddedParams)
    let result = await CVersion.get(version.id)
    return result
})()
..catch((err)=>{
    logger.error("更新參數失敗!");
    logger.error(version);
    logger.error(tagNames);
    logger.error(err);
})
  • 實現 async.each 的遍歷集合每個元素實現異步操做功能:
//源代碼
Notification.newNotifications= function(notifications, callback) {
    function iterator(notification, callback) {
        Notification.newNotification(notification, function(err, results) {
            logger.error(err);
            callback(err);
        });
    }

    async.each(notifications, iterator, function(err) {
        callback(err, null);
    });
}

新代碼:

//新代碼
Notification.newNotifications= function(notifications){
  notifications.forEach(async function(notification){
      try{
           await Notification.newNotification(notification)//異步操做
      } catch (err) {
           logger.error(err);
           return err;
        }    
  });
}

上述代碼須要說明的狀況是,在forEach 體內的每個元素的 await 都是併發執行的,由於這正好知足了 async.each 的特色,若是你但願的是數組元素繼發執行異步操做,也就是前文所提到的 async.eachSeries 的功能,你須要協程一個 for 循環而不是 forEach 的形式,相似以下代碼:

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);//異步數據庫操做
  }
}

若是你以爲上述併發集合操做使用 forEach 的方式依舊不太直觀,也能夠改成配合Promise.all的形式:

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

上述代碼現先對數組元素進行遍歷,將傳入了數組元素參數的一步操做封裝成爲一個數組,經過await Promise.all(promises)的形式進行併發操做。Tips: Promise.all 有自動將數組的每一個元素變成Promise對象的能力。

相關文章
相關標籤/搜索