成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專(zhuān)欄INFORMATION COLUMN

從“async”到async——Node異步流程控制總結(jié)

AbnerMing / 3443人閱讀

摘要:面對(duì)著線程相關(guān)的問(wèn)題,出現(xiàn)了協(xié)程。協(xié)程的特點(diǎn)在于是一個(gè)線程執(zhí)行,因此最大的優(yōu)勢(shì)就是協(xié)程極高的執(zhí)行效率。因?yàn)樽映绦蚯袚Q不是線程切換,而是由程序自身控制,因此,沒(méi)有線程切換的開(kāi)銷(xiāo),和多線程比,線程數(shù)量越多,協(xié)程的性能優(yōu)勢(shì)就越明顯。

Node的異步概念 理解異步非阻塞

提到Node,異步非阻塞會(huì)是第一個(gè)需要你理解的概念。很多人會(huì)把這實(shí)際上是兩個(gè)概念的詞混為一談,認(rèn)為異步就是非阻塞的,而同步就是阻塞的。從實(shí)際的效果出發(fā),異步IO和非阻塞IO實(shí)際上都能達(dá)到我們對(duì)于IO繁重的網(wǎng)絡(luò)應(yīng)用并行IO的追求。但是實(shí)際上這是兩個(gè)很不一樣的概念。

從操作系統(tǒng)的內(nèi)核角度出發(fā),I/O調(diào)用只有兩種方式,阻塞和非阻塞。二者的區(qū)別在于,對(duì)于使用阻塞IO調(diào)用,應(yīng)用程序需要等待IO的整個(gè)過(guò)程都全部完成,即完成整個(gè)IO目的,此期間CPU進(jìn)行等待,無(wú)法得到充分的利用。而對(duì)于使用非阻塞IO調(diào)用來(lái)說(shuō),應(yīng)用程序發(fā)起IO請(qǐng)求之后不等待數(shù)據(jù)就立即返回,接下來(lái)的CPU時(shí)間片可用于其他任務(wù),由于整個(gè)IO的過(guò)程并沒(méi)有完成,所以還需要使用輪詢(xún)技術(shù)去試探數(shù)據(jù)是否完整準(zhǔn)備好。關(guān)于輪詢(xún)技術(shù)細(xì)節(jié)和發(fā)展,此處不過(guò)多贅述,很推薦樸靈老師《深入淺出NodeJs》的第三章。

不難理解,從應(yīng)用程序的角度出發(fā),我不管你操作系統(tǒng)內(nèi)核是阻塞的IO調(diào)用還是非阻塞的IO調(diào)用,只要是我要的數(shù)據(jù)并沒(méi)有給我,那么這就是同步的,因?yàn)槲乙琅f是在等數(shù)據(jù)。所以對(duì)于這種情況下,應(yīng)用程序的那“一根筋”就可以選擇用同步還是異步的方式去面對(duì)該情況。同步即等待操作系統(tǒng)給到數(shù)據(jù)再進(jìn)行下面的代碼(單線程),異步即發(fā)出請(qǐng)求之后也立即返回,用某一種方式注冊(cè)未完成的任務(wù)(回調(diào)函數(shù))然后繼續(xù)往下執(zhí)行代碼。

理解進(jìn)程,線程,協(xié)程

為了使多個(gè)程序能夠并發(fā)(同一時(shí)刻只有一個(gè)在運(yùn)行,時(shí)間維度稍微拉長(zhǎng),就會(huì)感覺(jué)起來(lái)像多個(gè)同時(shí)運(yùn)行)便有了這個(gè)在操作系統(tǒng)中能夠獨(dú)立運(yùn)行并作為資源分配的基本單位。

進(jìn)程是資源分配的基本單位,進(jìn)程的調(diào)度涉及到的內(nèi)容比較多(存儲(chǔ)空間,CPU,I/O資源等,進(jìn)程現(xiàn)場(chǎng)保護(hù)),調(diào)度開(kāi)銷(xiāo)較大,在并發(fā)的切換過(guò)程效率較低。為了更高效的進(jìn)行調(diào)度,提出了比進(jìn)程更輕量的獨(dú)立運(yùn)行和調(diào)度的基本單位線程。最主要的一點(diǎn)同一個(gè)進(jìn)程的多個(gè)線程共享進(jìn)程的資源,這就會(huì)暴露出一個(gè)多線程編程中需要加入多線程的鎖機(jī)制來(lái)控制資源的互斥性(同時(shí)寫(xiě)變量沖突)。線程調(diào)度能大幅度減小調(diào)度的成本(相對(duì)于進(jìn)程來(lái)說(shuō)),線程的切換不會(huì)引起進(jìn)程的切換,但是畢竟還是有成本。

面對(duì)著線程相關(guān)的問(wèn)題,出現(xiàn)了協(xié)程。協(xié)程是用戶(hù)模式下的輕量級(jí)線程操作系統(tǒng)內(nèi)核對(duì)協(xié)程一無(wú)所知,協(xié)程的調(diào)度完全有應(yīng)用程序來(lái)控制,操作系統(tǒng)不管這部分的調(diào)度。

協(xié)程的特點(diǎn)在于是一個(gè)線程執(zhí)行,因此最大的優(yōu)勢(shì)就是協(xié)程極高的執(zhí)行效率。因?yàn)樽映绦蚯袚Q不是線程切換,而是由程序自身控制,因此,沒(méi)有線程切換的開(kāi)銷(xiāo),和多線程比,線程數(shù)量越多,協(xié)程的性能優(yōu)勢(shì)就越明顯。第二大優(yōu)勢(shì)就是不需要多線程的鎖機(jī)制,因?yàn)橹挥幸粋€(gè)線程,就也不存在同時(shí)寫(xiě)變量沖突,在協(xié)程中控制共享資源不加鎖,只需要判斷狀態(tài)就好了,所以執(zhí)行效率比多線程高很多。

依據(jù)上述概念本身我們可能可以得出一種暫時(shí)性的結(jié)論:考慮到利用多核CPU,并且充分發(fā)揮協(xié)程的高效率,又可獲得極高的性能,面向開(kāi)發(fā)人員最簡(jiǎn)單的方法是多進(jìn)程+協(xié)程,既充分利用多核

在Node中利用多核CPU的子進(jìn)程文檔

回調(diào)函數(shù)問(wèn)題

在Node中每一個(gè)異步的IO回調(diào)函數(shù)并不是由開(kāi)發(fā)人員所控制主動(dòng)執(zhí)行的。

那么對(duì)于Node的異步IO,在我們最常使用的異步回調(diào)的形式下,我們發(fā)出調(diào)用到回調(diào)函數(shù)執(zhí)行這中間發(fā)生了什么?

整個(gè)過(guò)程可簡(jiǎn)單的抽象成四個(gè)基本要素:IO線程池,觀察者,請(qǐng)求對(duì)象,以及事件循環(huán),盜用《深入淺出NodeJS》的Windows借用IOCP實(shí)現(xiàn)異步回調(diào)過(guò)程的一張圖片:

其中所要執(zhí)行的異步回調(diào)函數(shù)以及相關(guān)的所有狀態(tài)參數(shù)會(huì)被封裝成一個(gè)請(qǐng)求對(duì)象然后被推入到IO線程池中,當(dāng)操作系統(tǒng)執(zhí)行完IO得到結(jié)果之后會(huì)將數(shù)據(jù)放入請(qǐng)求對(duì)象中,并歸還當(dāng)前線程至線程池,通知IOCP完成了IO過(guò)程,然后事件循環(huán)IO觀察者中得到已經(jīng)可以執(zhí)行的請(qǐng)求對(duì)象中的回調(diào),灌注IO數(shù)據(jù)結(jié)果開(kāi)始執(zhí)行。

Node本身是多線程的,開(kāi)發(fā)人員的JS代碼單線程化身為一個(gè)老板,實(shí)現(xiàn)高效的異步邏輯依靠的是Node機(jī)制內(nèi)部的各個(gè)線程池,模擬出了一個(gè)異步非阻塞的特點(diǎn)。呈現(xiàn)在開(kāi)發(fā)人員面前的是表現(xiàn)形式為各種各樣的callback組成的一個(gè)原生編程風(fēng)格。

異步編程與“回調(diào)地獄”
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("還沒(méi)有獲取到content3!");
}

讀取的每一個(gè) .txt 文件中的內(nèi)容是要讀取的下一個(gè)文件的路徑地址,最后一個(gè)txt文件(test3.txt)中的內(nèi)容是“callback hell is not finished......”

打印結(jié)果:

主線程
還沒(méi)有獲取到content3!
callback hell is not finished......

可以理解為Node代碼一根筋的往下想盡快結(jié)束所謂的主線程,所以遇到設(shè)計(jì)異步的就自動(dòng)忽略并跳過(guò)為了往下執(zhí)行,所以出現(xiàn)了第一句非異步的打印操作,打印“主線程”,再往下執(zhí)行遇到需要打印 content3 這個(gè)變量的時(shí)候,主線程就“懵”了,因?yàn)槊臻g內(nèi)并沒(méi)有獲取到任何 content3 的數(shù)據(jù),甚至在主線程命名空間內(nèi)都沒(méi)有定義這個(gè)變量,如果不用 try-catch 那么應(yīng)該會(huì)報(bào) “content3 is not defined”的錯(cuò)誤。

此外,callback hell 一覽無(wú)余,一味地因?yàn)橐蕾?lài)而采用嵌套回調(diào)函數(shù)的方式,哪怕是上述代碼那么簡(jiǎn)單的一個(gè)原子性的操作都會(huì)被這種“橫向發(fā)展”的代碼和無(wú)休止的大括號(hào)嵌套讓業(yè)務(wù)邏輯代碼喪失掉可維護(hù)性和可讀性。

為了避免這種回調(diào)地獄,解決問(wèn)題的方案和第三方模塊就開(kāi)始層出不窮百花齊放了。

這個(gè)async不是ES2017的async

async是一個(gè)十分強(qiáng)大,功能十分全面提供異步編程解決法案的一個(gè)第三方npm模塊。也是我所接觸的公司中的項(xiàng)目中大范圍使用的。下面是關(guān)于這個(gè)模塊的常用函數(shù)使用介紹,先感受一下。

流程控制函數(shù)

async.parallel(tasks,callback)

tasks 可以是一個(gè)數(shù)組也可以是個(gè)對(duì)象,他的數(shù)組元素值或者對(duì)象的屬性值就是一個(gè)一個(gè)異步的方法。

parallel方法用于并行執(zhí)行多個(gè)方法,所有傳入的方法都是立即執(zhí)行,方法之間沒(méi)有數(shù)據(jù)傳遞。傳遞給最終callback的數(shù)組中的數(shù)據(jù)按照tasks中聲明的順序,而不是執(zhí)行完成的順序

//以數(shù)組形式傳入需要執(zhí)行的多個(gè)方法
async.parallel([
    function(callback){//每個(gè)function均需要傳入一個(gè)錯(cuò)誤優(yōu)先的callback
        // 異步函數(shù)1,比如 fs.readFile(path,callback)
    },
    function(callback){
        // 異步函數(shù)2
    }
],
//最終回調(diào) 
function(err, results){
    // 當(dāng)tasks中的任一方法發(fā)生錯(cuò)誤,即回調(diào)形式為callback("錯(cuò)誤信息")時(shí),錯(cuò)誤將被傳遞給err參數(shù),未發(fā)生錯(cuò)誤err參數(shù)為空
    if(err){
        console.log(err)
    }else{
        let one = results[0];
        let two = results[1];
        //你的各種操作
    }
    // results中為數(shù)組中,兩個(gè)方法的結(jié)果數(shù)組:[異步1的結(jié)果, 異步2的結(jié)果] ,即使第二個(gè)方法先執(zhí)行完成,其結(jié)果也是在第一個(gè)方法結(jié)果之后
});
 
//以object對(duì)象形式傳入需要執(zhí)行的多個(gè)方法
async.parallel({
    one: function(callback){
        // 異步函數(shù)1
    },
    two: function(callback){
        // 異步函數(shù)2
    }
},
function(err, results) {
    // 當(dāng)tasks中的任一方法發(fā)生錯(cuò)誤,即回調(diào)形式為callback("錯(cuò)誤信息")時(shí),錯(cuò)誤將被傳遞給err參數(shù),未發(fā)生錯(cuò)誤err參數(shù)為空
    // // results 現(xiàn)在等于: {one: 異步1的結(jié)果, two: 異步2的結(jié)果}
});

使用時(shí)所要注意的事項(xiàng):

當(dāng)tasks中的任一方法發(fā)生錯(cuò)誤時(shí),錯(cuò)誤將被傳遞給最終回調(diào)函數(shù)的err參數(shù),未發(fā)生錯(cuò)誤err參數(shù)為空。

tasks用數(shù)組的寫(xiě)法,即使第二個(gè)方法先執(zhí)行完成,其結(jié)果也是在第一個(gè)方法結(jié)果之后,兩個(gè)方法的結(jié)果數(shù)組:[異步1的結(jié)果, 異步2的結(jié)果]

個(gè)人感受:這個(gè)方法的大量使用讓我覺(jué)得當(dāng)一個(gè)要展示很多方面的信息的首頁(yè)時(shí),解耦成了代碼可讀性的最關(guān)鍵因素,親身體會(huì)的是使用這個(gè)方法在企業(yè)業(yè)務(wù)邏輯中理想情況是在 tasks 中注冊(cè)的并行任務(wù)得到的結(jié)果最好能夠直接使用,而不是在第一個(gè)async.parallel的最終回調(diào)中依舊需要依賴(lài)得到的結(jié)果再進(jìn)行下個(gè)系列的異步操作,因?yàn)檫@樣導(dǎo)致的結(jié)果直接就變成了代碼繼續(xù)向著橫向發(fā)展,比原生的 callback hell 并沒(méi)有要好到哪里去。篇幅原因就不展示實(shí)際代碼了,總之雖然結(jié)果流程得到了一個(gè)較為明確的控制,但是依舊沒(méi)有良好的可讀性

async.series(tasks,callback)

series方法用于依次執(zhí)行多個(gè)方法,一個(gè)方法執(zhí)行完畢后才會(huì)進(jìn)入下一方法,方法之間沒(méi)有數(shù)據(jù)傳遞!!。

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

//以數(shù)組形式傳入需要執(zhí)行的多個(gè)方法
async.series([
    function(callback){
       fs.readFile(path1,callback)
    },
    function(callback){
       fs.readFile(path2,callback)
    }
],
// 可選的最終回調(diào) 
function(err, results){
    // 當(dāng)tasks中的任一方法發(fā)生錯(cuò)誤,即回調(diào)形式為callback("錯(cuò)誤信息")時(shí),錯(cuò)誤將被傳遞給err參數(shù),未發(fā)生錯(cuò)誤err參數(shù)為空
    // results中為數(shù)組中兩個(gè)方法的結(jié)果數(shù)組:["one", "two"] 
});

這個(gè)方法在 tasks 中注冊(cè)的異步函數(shù)之間雖然沒(méi)有數(shù)據(jù)傳遞,但是這個(gè)方法控制了這些個(gè)異步方法的執(zhí)行順序,并且只要一個(gè)函數(shù)執(zhí)行失敗了接下來(lái)的函數(shù)就不會(huì)再執(zhí)行了,并且把 err 傳遞到最終的回調(diào)函數(shù)中的 err 參數(shù)中。正如它的名字 “series”所說(shuō),這個(gè)方法有點(diǎn)數(shù)據(jù)庫(kù)中的事務(wù)控制的意思,只不過(guò)原生不支持回滾罷了。

async.waterfall(tasks,callback)

waterfall方法與series方法類(lèi)似用于依次執(zhí)行多個(gè)方法,一個(gè)方法執(zhí)行完畢后才會(huì)進(jìn)入下一方法,不同與series方法的是,waterfall之間有數(shù)據(jù)傳遞,前一個(gè)函數(shù)的輸出為后一個(gè)函數(shù)的輸入。waterfall的多個(gè)方法只能以數(shù)組形式傳入,不支持object對(duì)象。

async.waterfall([
    function(callback) {
        callback(null, "one", "two");
    },
    function(arg1, arg2, callback) {
        // arg1 現(xiàn)在是 "one", arg2 現(xiàn)在是 "two" 
        callback(null, "three");
    },
    function(arg1, callback) {
        // arg1 現(xiàn)在是 "three" 
        callback(null, "done");
    }
], function (err, result) {
    //執(zhí)行的任務(wù)中方法回調(diào)err參數(shù)時(shí),將被傳遞至本方法的err參數(shù)
    // 參數(shù)result為最后一個(gè)方法的回調(diào)結(jié)果"done"     
});

因?yàn)?tasks 中注冊(cè)的異步函數(shù)數(shù)組中前一個(gè)函數(shù)的輸出作為后一個(gè)輸入,很自然的就可以想到可以通過(guò)前一個(gè)函數(shù)傳遞“處理成功信號(hào)”在第二個(gè)函數(shù)中進(jìn)行判斷來(lái)進(jìn)行一系列完整的簡(jiǎn)單類(lèi)似于事務(wù)控制的邏輯操作。

async.auto(tasks,callback)

auto方法根據(jù)傳入的任務(wù)類(lèi)型選擇最佳的執(zhí)行方式。不依賴(lài)于其它任務(wù)的方法將并發(fā)執(zhí)行,依賴(lài)于其它任務(wù)的方法將在其執(zhí)行完成后執(zhí)行。類(lèi)似于“依賴(lài)注入”概念。

async.auto({
    getData: function(callback){
         //一個(gè)取數(shù)據(jù)的方法
        // 與makeFolder方法并行執(zhí)行
        callback(null, "data", "converted to array");
    },
    makeFolder: function(callback){
        // 一個(gè)創(chuàng)建文件夾的方法
        // 與make_folder方法并行執(zhí)行
        callback(null, "folder");
    },
    writeFile: ["getData", "makeFolder", function(callback, results){
        // 此方法在等待getData方法和makeFolder執(zhí)行完成后執(zhí)行,并且在results中拿到依賴(lài)函數(shù)的數(shù)據(jù)
        callback(null, "filename");
    }],
    sendEmail: ["writeFile", function(callback, results){
        // 等待writeFile執(zhí)行完成后執(zhí)行,results中拿到依賴(lài)項(xiàng)的數(shù)據(jù)
        callback(null, {"file":results.writeFile, "email":"user@example.com"});
    }]
}, function(err, results) {
    console.log("err = ", err);
    console.log("results = ", results);
});

個(gè)人評(píng)價(jià):喜歡這種方法,有清晰的可讀性,依賴(lài)規(guī)則以及控制一目了然,很可惜的是在我們的代碼里面并沒(méi)有使用。缺點(diǎn)是相比較我們的最終解決方案的優(yōu)雅,這個(gè)還是會(huì)有可能嵌套很多層的大括號(hào)的方式有它本身的劣勢(shì)。

異步集合操作

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

對(duì)數(shù)組arr中的每一項(xiàng)執(zhí)行iterator操作。iterator方法中會(huì)傳一個(gè)當(dāng)前執(zhí)行的項(xiàng)及一個(gè)回調(diào)方法。each方法中所有對(duì)象是并行執(zhí)行的。對(duì)數(shù)組中每一項(xiàng)進(jìn)行 iterator 函數(shù)處理,如果有一項(xiàng)出錯(cuò)則最終的回調(diào)的 err 就回事該 err。但是,出錯(cuò)并不會(huì)影響到其他的數(shù)組元素執(zhí)行。

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)
    }
})

打印結(jié)果:

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

可見(jiàn),由于并發(fā)的原因,即是第二項(xiàng)出錯(cuò),也不會(huì)影響其余的元素執(zhí)行。如果想要讓數(shù)組中的元素按照順序執(zhí)行,并且一旦一個(gè)出錯(cuò),后面的數(shù)組元素都將不會(huì)執(zhí)行的情況應(yīng)該用另一個(gè)函數(shù) async.eachSeeries(arr,iterator(item, callback),callback),用法什么的都一樣,這里就不贅述了。

此外,each方法的最終回調(diào)函數(shù)可以看出來(lái)的是,并不會(huì)被傳入任何結(jié)果,所以最終的回調(diào)函數(shù)就只有一個(gè)參數(shù)那就是 err,如果想要向最終回調(diào)函數(shù)中傳入某些結(jié)果那么還要用到接下來(lái)介紹的 asycnc.map()

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

map方法使用方式和each完全一樣,與each方法不同的是,map方法用于操作對(duì)象的轉(zhuǎn)換,轉(zhuǎn)換后的新的結(jié)果集會(huì)被傳遞至最終回調(diào)方法中(不出錯(cuò)的情況下)呈現(xiàn)一個(gè)新的數(shù)組的形似。

同樣的是,map也是并行操作,如需按順序并且出錯(cuò)就停止則需要使用 async.mapSeries

向Promise的過(guò)渡 Promise基礎(chǔ)簡(jiǎn)要介紹

一個(gè)簡(jiǎn)單清晰的例子:

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形式的異步函數(shù)多了一步封裝包裹的過(guò)程。Promise是一個(gè)對(duì)象,可以把它看做是一個(gè)包含著異步函數(shù)可能出現(xiàn)的結(jié)果(成功或者失?。╡rr))的“異步狀態(tài)小球”。得到了這個(gè)小球你就能用 then 去弄他,用 catch 去捕獲它的失敗。簡(jiǎn)單的概括,也僅此而已?;谶@個(gè)小球,我們就能得到所謂的“現(xiàn)代異步處理方案”了,后話。

前端 Promisify Ajax請(qǐng)求:

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基本上都是基于一個(gè) callback 形式的函數(shù),我們想用 Promise ,難不成甚至原生的這些最原始的函數(shù)都要我們手動(dòng)去進(jìn)行 return 一個(gè) Promise 對(duì)象的改造?其實(shí)不是這樣的,Node 風(fēng)格的 callback 都遵從著“錯(cuò)誤優(yōu)先”的回到函數(shù)方案,即形如(err,res)=>{},并且回調(diào)函數(shù)都是最后一個(gè)參數(shù),他們的形式都是一致的。所以Node的原生util模塊提供了一個(gè)方便我們將函數(shù) 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);
    })

注意,這個(gè)原生工具會(huì)對(duì)原生回調(diào)的結(jié)果進(jìn)行封裝,如果在最后的回調(diào)函數(shù)中除了 err 參數(shù)之外,還有不止一個(gè)結(jié)果的情況,那么 util.promisify 會(huì)將結(jié)果都統(tǒng)一封裝進(jìn)一個(gè)對(duì)象之中。

用Promise提供方法應(yīng)對(duì)不同的情況

實(shí)際代碼邏輯中我們可能會(huì)面對(duì)各種異步流程控制的情況,像是之前介紹 async 模塊一樣,一種很常見(jiàn)的情況就是有很多的異步方法是可以同時(shí)并發(fā)發(fā)起請(qǐng)求的,即互相不依賴(lài)對(duì)方的結(jié)果,async.parallel的效果那樣。Promise 除了封裝異步之外還未我們提供了一些原生方法去面對(duì)類(lèi)似這樣的情況:

知識(shí)準(zhǔn)備

Promise.resolve(value)

它是下面這段代碼的語(yǔ)法糖:

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

注意點(diǎn),在 then 調(diào)用的時(shí)候即便一個(gè)promise對(duì)象是立即進(jìn)入完成狀態(tài)的,那Promise的 then 調(diào)用也是異步的,這是為了避免同步和異步之間狀態(tài)出現(xiàn)了模糊。所以你可以認(rèn)為,Promise 只能是異步的,用接下的代碼說(shuō)明:

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

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

console.log("3.最后書(shū)寫(xiě)");

上面的代碼,打印的結(jié)果如下:

1.構(gòu)造Promise函數(shù)
3.最后書(shū)寫(xiě)
2.成功態(tài)
ray is handsome

promise 可以鏈?zhǔn)?then ,每一個(gè) then 之后都會(huì)產(chǎn)生一個(gè)新的 promise 對(duì)象,在 then 鏈中前一個(gè) then 這種可以通過(guò) return的方式想下一個(gè) then 傳遞值,這個(gè)值會(huì)自動(dòng)調(diào)用 promise.resolve()轉(zhuǎn)化成一個(gè)promise對(duì)象,代碼說(shuō)明吧:

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

上面的代碼答應(yīng)的結(jié)果:

1
first那里傳下來(lái)的2
second那里傳下來(lái)的3
3

此外 then 鏈中應(yīng)該添加 catch 捕獲異常,某一個(gè) then 中出現(xiàn)了錯(cuò)誤則執(zhí)行鏈會(huì)跳過(guò)后來(lái)的 then 直接進(jìn)入 catch

得到 async.parallel同樣的效果

Promise 提供了一個(gè)原生方法 Promise.all(arr),其中arr是一個(gè)由 promise 對(duì)象組成的一個(gè)數(shù)組。該方法可以實(shí)現(xiàn)讓傳入該方法的數(shù)組中的 promise 同時(shí)執(zhí)行,并在所有的 promise 都有了最終的狀態(tài)之后,才會(huì)調(diào)用接下來(lái)的 then 方法,并且得到的結(jié)果和在數(shù)組中注冊(cè)的結(jié)果保持一致。看下面的代碼:

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);
    })

上面的代碼最終會(huì)打印,即是按順序的三個(gè)txt文件里面的內(nèi)容組成的數(shù)組:

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

對(duì)比 async.parallel的用法,發(fā)現(xiàn)得到相同的結(jié)果。

此外,與 Promise.all方法相對(duì)應(yīng)的還有一個(gè)Promise.race,該方法與all用法相同,同樣是傳入一個(gè)由 promise 對(duì)象組成的數(shù)組,你可以把上面的代碼中的 all 直接換成 race 看看是什么效果。沒(méi)錯(cuò),對(duì)于指導(dǎo) race 這個(gè)英文單詞意思的可能已經(jīng)猜出來(lái)了,race 競(jìng)爭(zhēng),賽跑,就是只要數(shù)組中有一個(gè) promise 到達(dá)最終態(tài),該方法的 then 就會(huì)執(zhí)行。所以該代碼有可能會(huì)出現(xiàn)"1","2","3"中的任何一個(gè)字符串。

至此,我們解決了要改造的代碼的第一個(gè)問(wèn)題,那就是多異步的同時(shí)執(zhí)行,那么之前 async 模塊介紹的其他的的功能在實(shí)際運(yùn)用中也很常見(jiàn)的幾個(gè)場(chǎng)景,類(lèi)似順序執(zhí)行異步函數(shù),異步集合操作要怎么使用新的方案模擬出來(lái)呢?真正的原生 async要登場(chǎng)了。

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

在開(kāi)始介紹 async 之前,想先聊一種情況。

基于 Promise 的這一套看似可以讓代碼“豎著寫(xiě)”,可以很好的解決“callbackHell”回調(diào)地獄的窘境,但是上述所有的例子都是簡(jiǎn)單場(chǎng)景下。在基于 Promise 的 then 鏈中我們不難發(fā)現(xiàn),雖然一層層往下的 then 鏈可以向下一層傳遞本層處理好的數(shù)據(jù),但是這種鏈條并不能跨層使用數(shù)據(jù),就是說(shuō)如果第3層的 then 想直接使用第一層的結(jié)果必須有一個(gè)前提就是第二層不僅將自己處理好的數(shù)據(jù) return 給第三層,同時(shí)還要把第一層傳下來(lái)的再一次傳給第三層使用。不然還有一種方式,那就是我們從回調(diào)地獄陷入另一種地獄 “Promise地獄”。

借用這篇博客 的一個(gè)操作 mongoDB 場(chǎng)景例子說(shuō)明:

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);
})

如果我想要在最后一個(gè) then 中得到 db 對(duì)象用來(lái)執(zhí)行 db.close()關(guān)閉數(shù)據(jù)庫(kù)操作,我只能選擇讓每一層都傳遞這個(gè) db 對(duì)象直至我使用操作 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()返回的是一個(gè)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);
})

看上去不是那么明顯,但是已經(jīng)出現(xiàn)了 then 里面嵌套 then 了,操作一多直接一夜回到解放前,再一次喪失了讓人想看代碼的欲望。OK,用傳說(shuō)中的 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);
});

各種異步寫(xiě)的像同步了,async(異步)關(guān)鍵字聲明,告訴讀代碼的這是一個(gè)包含了各種異步操作的函數(shù),await(得等它)關(guān)鍵字說(shuō)明后面的是個(gè)異步操作,卡死了等他執(zhí)行完再往下。這個(gè)語(yǔ)義以及視覺(jué)確實(shí)沒(méi)法否認(rèn)這可能是“最好的”異步解決方案了吧。

不得不提的 co 模塊

眾所周知的是 async 函數(shù)式 generator 的語(yǔ)法糖,generator 在異步流程控制中的執(zhí)行依賴(lài)于執(zhí)行器,co 模塊就是一個(gè) generator 的執(zhí)行器,在真正介紹和使用 async 解決法案之前有必要簡(jiǎn)單了解一下大名鼎鼎的 co 模塊。

什么是 generator,詳細(xì)請(qǐng)參考Ecmascript6 入門(mén)

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());
};
// 執(zhí)行生成器,返回一個(gè)生成器內(nèi)部的指針
var g = gen();
//手動(dòng) generator 執(zhí)行器
g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
})

上述代碼采用 generator 的方式在 yeild 關(guān)鍵字后面封裝了異步操作并通過(guò) next()去手動(dòng)執(zhí)行它。調(diào)用 g.next() 是去執(zhí)行 yield 后面的異步,這個(gè)方案就是經(jīng)典的異步的“協(xié)程”(多個(gè)線程互相協(xié)作,完成異步任務(wù))處理方案。

協(xié)程執(zhí)行步驟:

協(xié)程A開(kāi)始執(zhí)行。

協(xié)程A執(zhí)行到一半,進(jìn)入暫停,執(zhí)行權(quán)轉(zhuǎn)移到協(xié)程B。

(一段時(shí)間后)協(xié)程B交還執(zhí)行權(quán)。

協(xié)程A恢復(fù)執(zhí)行。

協(xié)程遇到 yield 命令就暫停 等到執(zhí)行權(quán)返回,再?gòu)臅和5牡胤嚼^續(xù)往后執(zhí)行。

翻譯上述代碼:

gen()執(zhí)行后返回一個(gè)生成器的內(nèi)部執(zhí)行指針,gen 生成器就是一個(gè)協(xié)程。

gen.next()讓生成器內(nèi)部開(kāi)始執(zhí)行代碼到遇到 yield 執(zhí)行 yield 后,就暫停該協(xié)程,并且交出執(zhí)行權(quán),此時(shí)執(zhí)行權(quán)落到了JS主線程的手里,即開(kāi)始執(zhí)行 Promise 的 then 解析。

then 的回調(diào)里取得了該異步數(shù)據(jù)結(jié)果,調(diào)用g.next(data)通過(guò)網(wǎng)next()函數(shù)傳參的形式,將結(jié)果返回給生成器的f1變量。

依次回調(diào)類(lèi)推。

說(shuō)明:

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

引出一個(gè)問(wèn)題: 我們不能每一次用 generator 處理異步都要手寫(xiě) generator 的 then 回調(diào)執(zhí)行器,該格式相同,每次都是調(diào)用.next(),所以可以用遞歸函數(shù)封裝成一個(gè)函數(shù):

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);

上述執(zhí)行器的函數(shù)編寫(xiě) co 模塊考慮周全的寫(xiě)好了,co模塊源碼

你只需要:

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

yield 后面的是并發(fā)。

此時(shí)我們來(lái)對(duì)比 async 寫(xiě)法:)

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

async 函數(shù)就是將 Generator 函數(shù)的星號(hào)(*)替換成 async,將 yield 替換成 await,僅此而已。并且它不需要額外的執(zhí)行器,因?yàn)樗?strong>自帶 Generator 執(zhí)行器

本質(zhì)上其實(shí)并沒(méi)有脫離“協(xié)程”異步的處理方式

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("出錯(cuò)了")
})



console.log("主線程")

打印結(jié)果會(huì)先輸出“主線程”。

async 解決方案

前文我們通過(guò) Promise.all()解決了 async.paralle()的功能,現(xiàn)在我們來(lái)看看用 Promise 配合原生 async 來(lái)達(dá)到“async”模塊的其他功能。

實(shí)現(xiàn) async.series 順序執(zhí)行異步函數(shù)

//源代碼
async.series([
        function(callback) {
            if (version.other_parameters != otherParams) { // 更新其他參數(shù)
                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("更新電路圖參數(shù)失?。?);
            logger.error(version);
            logger.error(tagNames);
            logger.error(err);
            callback(err);
        } else {
            callback(null, results[3].parameters);
        }
    });


//新代碼

(async function(){
    if (version.other_parameters != otherParams) { // 更新其參數(shù)
        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("更新參數(shù)失??!");
    logger.error(version);
    logger.error(tagNames);
    logger.error(err);
})

實(shí)現(xiàn) async.each 的遍歷集合每一個(gè)元素實(shí)現(xiàn)異步操作功能:

//源代碼
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;
        }    
  });
}

上述代碼需要說(shuō)明的情況是,在forEach 體內(nèi)的每一個(gè)元素的 await 都是并發(fā)執(zhí)行的,因?yàn)檫@正好滿(mǎn)足了 async.each 的特點(diǎn),如果你希望的是數(shù)組元素繼發(fā)執(zhí)行異步操作,也就是前文所提到的 async.eachSeries 的功能,你需要協(xié)程一個(gè) for 循環(huán)而不是 forEach 的形式,類(lèi)似如下代碼:

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

  for (let doc of docs) {
    await db.post(doc);//異步數(shù)據(jù)庫(kù)操作
  }
}

如果你覺(jué)得上述并發(fā)集合操作使用 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);
}

上述代碼現(xiàn)先對(duì)數(shù)組元素進(jìn)行遍歷,將傳入了數(shù)組元素參數(shù)的一步操作封裝成為一個(gè)數(shù)組,通過(guò)await Promise.all(promises)的形式進(jìn)行并發(fā)操作。Tips: Promise.all 有自動(dòng)將數(shù)組的每個(gè)元素變成Promise對(duì)象的能力。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.hztianpu.com/yun/93547.html

相關(guān)文章

  • Node.js設(shè)計(jì)模式》基于回調(diào)的異步控制

    摘要:編寫(xiě)異步代碼可能是一種不同的體驗(yàn),尤其是對(duì)異步控制流而言?;卣{(diào)函數(shù)的準(zhǔn)則在編寫(xiě)異步代碼時(shí),要記住的第一個(gè)規(guī)則是在定義回調(diào)時(shí)不要濫用閉包。為回調(diào)創(chuàng)建命名函數(shù),避免使用閉包,并將中間結(jié)果作為參數(shù)傳遞。 本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書(shū)筆記,在GitHub連載更新,同步翻譯版鏈接。 歡迎關(guān)注我的專(zhuān)欄,之后的博文將在專(zhuān)...

    Chiclaim 評(píng)論0 收藏0
  • JavaScript異步編程解決方案筆記

    摘要:異步編程解決方案筆記最近讀了樸靈老師的深入淺出中異步編程一章,并參考了一些有趣的文章。另外回調(diào)函數(shù)中的也失去了意義,這會(huì)使我們的程序必須依賴(lài)于副作用。 JavaScript 異步編程解決方案筆記 最近讀了樸靈老師的《深入淺出NodeJS》中《異步編程》一章,并參考了一些有趣的文章。在此做個(gè)筆記,記錄并鞏固學(xué)到的知識(shí)。 JavaScript異步編程的兩個(gè)核心難點(diǎn) 異步I/O、事件驅(qū)動(dòng)使得...

    dmlllll 評(píng)論0 收藏0
  • 總結(jié)javascript基礎(chǔ)概念(二):事件隊(duì)列循環(huán)

    摘要:而事件循環(huán)是主線程中執(zhí)行棧里的代碼執(zhí)行完畢之后,才開(kāi)始執(zhí)行的。由此產(chǎn)生的異步事件執(zhí)行會(huì)作為任務(wù)隊(duì)列掛在當(dāng)前循環(huán)的末尾執(zhí)行。在下,觀察者基于監(jiān)聽(tīng)事件的完成情況在下基于多線程創(chuàng)建。 主要問(wèn)題: 1、JS引擎是單線程,如何完成事件循環(huán)的? 2、定時(shí)器函數(shù)為什么計(jì)時(shí)不準(zhǔn)確? 3、回調(diào)與異步,有什么聯(lián)系和不同? 4、ES6的事件循環(huán)有什么變化?Node中呢? 5、異步控制有什么難點(diǎn)?有什么解決方...

    zhkai 評(píng)論0 收藏0
  • NodeJs爬蟲(chóng)抓取古代典籍,共計(jì)16000個(gè)頁(yè)面心得體會(huì)總結(jié)及項(xiàng)目分享

    摘要:目前這個(gè)爬蟲(chóng)還是比較簡(jiǎn)單的類(lèi)型的,直接抓取頁(yè)面,然后在頁(yè)面中提取數(shù)據(jù),保存數(shù)據(jù)到數(shù)據(jù)庫(kù)??偨Y(jié)寫(xiě)這個(gè)項(xiàng)目其實(shí)主要的難點(diǎn)在于程序穩(wěn)定性的控制,容錯(cuò)機(jī)制的設(shè)置,以及錯(cuò)誤的記錄,目前這個(gè)項(xiàng)目基本能夠?qū)崿F(xiàn)直接運(yùn)行一次性跑通整個(gè)流程。 前言 之前研究數(shù)據(jù),零零散散的寫(xiě)過(guò)一些數(shù)據(jù)抓取的爬蟲(chóng),不過(guò)寫(xiě)的比較隨意。有很多地方現(xiàn)在看起來(lái)并不是很合理 這段時(shí)間比較閑,本來(lái)是想給之前的項(xiàng)目做重構(gòu)的。后來(lái) 利用這...

    legendmohe 評(píng)論0 收藏0
  • Node.js設(shè)計(jì)模式》基于ES2015+的回調(diào)控制

    摘要:以下展示它是如何工作的函數(shù)使用構(gòu)造函數(shù)創(chuàng)建一個(gè)新的對(duì)象,并立即將其返回給調(diào)用者。在傳遞給構(gòu)造函數(shù)的函數(shù)中,我們確保傳遞給,這是一個(gè)特殊的回調(diào)函數(shù)。 本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書(shū)筆記,在GitHub連載更新,同步翻譯版鏈接。 歡迎關(guān)注我的專(zhuān)欄,之后的博文將在專(zhuān)欄同步: Encounter的掘金專(zhuān)欄 知乎專(zhuān)欄...

    LiuRhoRamen 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<