在上一節中,我們實現了批量文件讀取,即循環異步函數的併發和有序操作。
總結爲:
併發操作可以在Array.forEach 和 Array.map 中進行;
有序操作則在循環中對Promise加入await關鍵字,意味着同步等待結果,並用async修飾整個函數;
由於需要實現在批量讀取完成後回調的功能,例如提示操作完成,統計結果等,我們需要在每個循環中判斷是否讀取到了最後一個
文件,採用的方法是計數器。儘管它在NodeJS單線程環境下,不會造成由於多線程造成的變量讀取衝突,但這種方法顯然是醜陋且繁雜的。
在這裏,我們引入thenjs庫來實現批量異步操作的順序和無序執行以及回調。
這一節我們將用網絡請求代替文件讀取來模擬批量的異步操作。
我們用Rap來模擬http請求並得到自定義數據:
對於NodeJS端http請求的發送和獲取,常用的庫有request.js
, node-fetch
等,筆者都實現了一遍,在這裏以node-fetch 爲例來實現。
先貼代碼再講解:
thenjs實現事件隊列的併發(無序調用):
import fetch from 'node-fetch';
import bodyParser from 'body-parser';
import thenjs from 'thenjs';
import request from "request";
import fs from 'fs';
const api = 'http://rapapi.org/mockjsdata/18728/fetchtest';
let requests = [];
for(let i = 0; i < 10; ++i){
let obj = {};
obj.url = api;
obj.count = i + 1;
requests.push(obj);
}
const batchRequst = (urls) => {
thenjs.each(urls, (defer, obj) => {
fetch(obj.url)
.then(res => res.json())
.then(res => {
console.log(JSON.stringify(res) + ' ' + obj.count);
defer(null, res);
})
.catch(err => defer(err, res));
}).then((defer, result) => {
console.log(result);
console.log('Done...')
}).fail((defer, err) => {
console.log(err);
})
}
batchRequst(requests);
console.log('Start fetching...')
輸出:
Start fetching...
{"data":{"name":"Paul Robinson","id":65133}} 4
{"data":{"name":"Margaret Lee","id":70397}} 2
{"data":{"name":"Joseph Davis","id":46583}} 6
{"data":{"name":"Kenneth Johnson","id":51935}} 5
{"data":{"name":"Timothy Garcia","id":46554}} 3
{"data":{"name":"Maria Anderson","id":93948}} 10
{"data":{"name":"Elizabeth Martin","id":19124}} 8
{"data":{"name":"Betty Hall","id":14796}} 7
{"data":{"name":"Shirley Robinson","id":92156}} 9
{"data":{"name":"Michelle Hernandez","id":48647}} 1
[ { data: { name: 'Michelle Hernandez', id: 48647 } },
{ data: { name: 'Margaret Lee', id: 70397 } },
{ data: { name: 'Timothy Garcia', id: 46554 } },
{ data: { name: 'Paul Robinson', id: 65133 } },
{ data: { name: 'Kenneth Johnson', id: 51935 } },
{ data: { name: 'Joseph Davis', id: 46583 } },
{ data: { name: 'Betty Hall', id: 14796 } },
{ data: { name: 'Elizabeth Martin', id: 19124 } },
{ data: { name: 'Shirley Robinson', id: 92156 } },
{ data: { name: 'Maria Anderson', id: 93948 } } ]
Done...
Done...
將上述代碼中的 each
改爲 eachSeries
,得到結果爲:
Start fetching...
{"data":{"id":72083,"name":"Linda Young"}} 1
{"data":{"id":33670,"name":"Nancy Thomas"}} 2
{"data":{"id":75790,"name":"Jennifer Moore"}} 3
{"data":{"id":82450,"name":"Carol Thomas"}} 4
{"data":{"id":45270,"name":"Deborah Garcia"}} 5
{"data":{"id":33547,"name":"Kevin Walker"}} 6
{"data":{"id":57576,"name":"William Walker"}} 7
{"data":{"id":86271,"name":"Richard Harris"}} 8
{"data":{"id":45700,"name":"Helen Perez"}} 9
{"data":{"id":59519,"name":"Maria Davis"}} 10
[ { data: { id: 72083, name: 'Linda Young' } },
{ data: { id: 33670, name: 'Nancy Thomas' } },
{ data: { id: 75790, name: 'Jennifer Moore' } },
{ data: { id: 82450, name: 'Carol Thomas' } },
{ data: { id: 45270, name: 'Deborah Garcia' } },
{ data: { id: 33547, name: 'Kevin Walker' } },
{ data: { id: 57576, name: 'William Walker' } },
{ data: { id: 86271, name: 'Richard Harris' } },
{ data: { id: 45700, name: 'Helen Perez' } },
{ data: { id: 59519, name: 'Maria Davis' } } ]
Done...
如thenjs的官網介紹所說,它能將任何同步或異步回調函數轉化爲then的鏈式調用。它的主要特徵如下:
可以像標準的 Promise 那樣,把N多異步回調函數寫成一個長長的 then 鏈,並且比 Promise 更簡潔自然。因爲如果使用標準 Promise 的 then 鏈,其中的異步函數都必須轉換成 Promise,Thenjs 則無需轉換,像使用 callback
一樣執行異步函數即可。可以像 async那樣實現同步或異步隊列函數,並且比 async 更方便。因爲 async 的隊列是一個個獨立體,而 Thenjs 的隊列在 Thenjs 鏈上,可形成鏈式調用。
強大的 Error 機制,可以捕捉任何同步或異步的異常錯誤,甚至是位於異步函數中的語法錯誤。並且捕捉的錯誤任君處置。
開啓debug模式,可以把每一個then鏈運行結果輸出到debug函數(未定義debug函數則用 console.log),方便調試。
對於第三點和第四點,筆者尚未在程序中體現。因此着重介紹第一點和第二點。對於事件隊列的操作,本節主要應用瞭如下兩個Api:
Thenjs.each(array, iterator, [debug])
將 array 中的值應用於 iterator 函數(同步或異步),並行執行。返回一個新的 Thenjs 對象。
Thenjs.eachSeries(array, iterator, [debug])
將 array 中的值應用於 iterator 函數(同步或異步),串行執行。返回一個新的 Thenjs 對象。
thenjs.each(urls, (defer, obj) => {
fetch(obj.url)
.then(res => res.json())
.then(res => {
console.log(JSON.stringify(res) + ' ' + obj.count);
defer(null, res);
})
.catch(err => defer(err, res));
}).then((defer, result) => {
console.log(result);
console.log('Done...')
}).fail((defer, err) => {
console.log(err);
})
一般來說[debug]參數不常用。iterator是用戶自定義的對每個數組元素執行的函數,一般形式爲 function(defer, value)
。
在each
或eachSeries
函數中使用了fetch
這一異步獲取網絡數據的操作,並在then函數中捕獲http請求的結果,顯示,並調用defer
函數。對於一般形如 asyncfunction(arg, (err, data) => {})
的NodeJS異步函數來說,只要在回調函數中調用defer
函數即可。
defer
爲一個回調函數,形式一般爲 function(err, res)
該函數返回一個thenjs對象供下一步操作,這也是thenjs能實現鏈式調用的關鍵步驟。若無錯誤,則第一個參數爲null;res
則表示批量請求結束後傳入回調函數的數據。
第二個.then函數,傳入的是形如(defer, result) => {}
的函數,result收集了在defer(err, res)
中傳入的結果並組裝成數列。無論是同步還是異步(eachSeries or each)的形式,得到數組的排列順序都和原始數組的請求數據一一對應。讀者可觀察打印出的result
來得到上述結論。
歸納起來,thenjs實現批量異步請求的函數模板爲:
thenjs.[each|eachSeries](array, (defer, element) => {
// implement async funtion here
// callback: defer(err||null, res)
}).then((defer, result) => {
// to do something
}.fail((defer, err) => {
// catch err
})
一般來說,併發比順序執行的效率高很多,且應用也更廣,這也是NodeJS佔優勢的地方。由於許多服務器的限流,如果發送的http請求併發數量過大,可能會被服務器端禁止訪問,這涉及到爬蟲和反爬蟲的策略,在此無法細數。但一個較爲直接的解決方案是採用 setInterval(callback, millisecond)
方法向服務器端定時發送請求,在這裏也不再贅述。
對於併發執行,也可採用 Promise.all等方法在實現,讀者可自行研讀。
自此,三節有關於NodeJS異步回調實現的博客已經全部完成。
從單個的異步回調,到批量請求的併發及有序實現,以文件讀取和http請求爲例進行了實現並講解,希望我的經驗能幫助到大家。
由於筆者理工科出生,且這是我第一次發技術類博客,可能語言文字組織的不夠嚴密,望各位博友多提寶貴意見,對於文章中出現的錯誤,也請各位斧正。