摘要:面對著線程相關的問題,出現了協程。協程的特點在于是一個線程執行,因此最大的優勢就是協程極高的執行效率。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。
Node的異步概念 理解異步非阻塞
提到Node,異步非阻塞會是第一個需要你理解的概念。很多人會把這實際上是兩個概念的詞混為一談,認為異步就是非阻塞的,而同步就是阻塞的。從實際的效果出發,異步IO和非阻塞IO實際上都能達到我們對于IO繁重的網絡應用并行IO的追求。但是實際上這是兩個很不一樣的概念。
從操作系統的內核角度出發,I/O調用只有兩種方式,阻塞和非阻塞。二者的區別在于,對于使用阻塞IO調用,應用程序需要等待IO的整個過程都全部完成,即完成整個IO目的,此期間CPU進行等待,無法得到充分的利用。而對于使用非阻塞IO調用來說,應用程序發起IO請求之后不等待數據就立即返回,接下來的CPU時間片可用于其他任務,由于整個IO的過程并沒有完成,所以還需要使用輪詢技術去試探數據是否完整準備好。關于輪詢技術細節和發展,此處不過多贅述,很推薦樸靈老師《深入淺出NodeJs》的第三章。
不難理解,從應用程序的角度出發,我不管你操作系統內核是阻塞的IO調用還是非阻塞的IO調用,只要是我要的數據并沒有給我,那么這就是同步的,因為我依舊是在等數據。所以對于這種情況下,應用程序的那“一根筋”就可以選擇用同步還是異步的方式去面對該情況。同步即等待操作系統給到數據再進行下面的代碼(單線程),異步即發出請求之后也立即返回,用某一種方式注冊未完成的任務(回調函數)然后繼續往下執行代碼。
理解進程,線程,協程為了使多個程序能夠并發(同一時刻只有一個在運行,時間維度稍微拉長,就會感覺起來像多個同時運行)便有了這個在操作系統中能夠獨立運行并作為資源分配的基本單位。
進程是資源分配的基本單位,進程的調度涉及到的內容比較多(存儲空間,CPU,I/O資源等,進程現場保護),調度開銷較大,在并發的切換過程效率較低。為了更高效的進行調度,提出了比進程更輕量的獨立運行和調度的基本單位線程。最主要的一點同一個進程的多個線程共享進程的資源,這就會暴露出一個多線程編程中需要加入多線程的鎖機制來控制資源的互斥性(同時寫變量沖突)。線程調度能大幅度減小調度的成本(相對于進程來說),線程的切換不會引起進程的切換,但是畢竟還是有成本。
面對著線程相關的問題,出現了協程。協程是用戶模式下的輕量級線程,操作系統內核對協程一無所知,協程的調度完全有應用程序來控制,操作系統不管這部分的調度。
協程的特點在于是一個線程執行,因此最大的優勢就是協程極高的執行效率。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。第二大優勢就是不需要多線程的鎖機制,因為只有一個線程,就也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。
依據上述概念本身我們可能可以得出一種暫時性的結論:考慮到利用多核CPU,并且充分發揮協程的高效率,又可獲得極高的性能,面向開發人員最簡單的方法是多進程+協程,既充分利用多核
在Node中利用多核CPU的子進程文檔
回調函數問題在Node中每一個異步的IO回調函數并不是由開發人員所控制主動執行的。
那么對于Node的異步IO,在我們最常使用的異步回調的形式下,我們發出調用到回調函數執行這中間發生了什么?
整個過程可簡單的抽象成四個基本要素:IO線程池,觀察者,請求對象,以及事件循環,盜用《深入淺出NodeJS》的Windows借用IOCP實現異步回調過程的一張圖片:
其中所要執行的異步回調函數以及相關的所有狀態參數會被封裝成一個請求對象然后被推入到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的asyncasync是一個十分強大,功能十分全面提供異步編程解決法案的一個第三方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 后面的異步,這個方案就是經典的異步的“協程”(多個線程互相協作,完成異步任務)處理方案。
協程執行步驟:
協程A開始執行。
協程A執行到一半,進入暫停,執行權轉移到協程B。
(一段時間后)協程B交還執行權。
協程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對象的能力。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93547.html
摘要:編寫異步代碼可能是一種不同的體驗,尤其是對異步控制流而言。回調函數的準則在編寫異步代碼時,要記住的第一個規則是在定義回調時不要濫用閉包。為回調創建命名函數,避免使用閉包,并將中間結果作為參數傳遞。 本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。 歡迎關注我的專欄,之后的博文將在專...
摘要:異步編程解決方案筆記最近讀了樸靈老師的深入淺出中異步編程一章,并參考了一些有趣的文章。另外回調函數中的也失去了意義,這會使我們的程序必須依賴于副作用。 JavaScript 異步編程解決方案筆記 最近讀了樸靈老師的《深入淺出NodeJS》中《異步編程》一章,并參考了一些有趣的文章。在此做個筆記,記錄并鞏固學到的知識。 JavaScript異步編程的兩個核心難點 異步I/O、事件驅動使得...
摘要:而事件循環是主線程中執行棧里的代碼執行完畢之后,才開始執行的。由此產生的異步事件執行會作為任務隊列掛在當前循環的末尾執行。在下,觀察者基于監聽事件的完成情況在下基于多線程創建。 主要問題: 1、JS引擎是單線程,如何完成事件循環的? 2、定時器函數為什么計時不準確? 3、回調與異步,有什么聯系和不同? 4、ES6的事件循環有什么變化?Node中呢? 5、異步控制有什么難點?有什么解決方...
摘要:目前這個爬蟲還是比較簡單的類型的,直接抓取頁面,然后在頁面中提取數據,保存數據到數據庫。總結寫這個項目其實主要的難點在于程序穩定性的控制,容錯機制的設置,以及錯誤的記錄,目前這個項目基本能夠實現直接運行一次性跑通整個流程。 前言 之前研究數據,零零散散的寫過一些數據抓取的爬蟲,不過寫的比較隨意。有很多地方現在看起來并不是很合理 這段時間比較閑,本來是想給之前的項目做重構的。后來 利用這...
摘要:以下展示它是如何工作的函數使用構造函數創建一個新的對象,并立即將其返回給調用者。在傳遞給構造函數的函數中,我們確保傳遞給,這是一個特殊的回調函數。 本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。 歡迎關注我的專欄,之后的博文將在專欄同步: Encounter的掘金專欄 知乎專欄...
閱讀 2261·2021-10-09 09:41
閱讀 3409·2021-09-13 10:34
閱讀 1920·2019-08-30 12:59
閱讀 557·2019-08-29 17:27
閱讀 1063·2019-08-29 16:07
閱讀 2956·2019-08-29 13:15
閱讀 1306·2019-08-29 13:14
閱讀 1562·2019-08-26 12:18