NodeJS 異步操作:更爲優雅的實現:thenjs

在上一節中,我們實現了批量文件讀取,即循環異步函數的併發和有序操作。
總結爲:
併發操作可以在Array.forEach 和 Array.map 中進行;
有序操作則在循環中對Promise加入await關鍵字,意味着同步等待結果,並用async修飾整個函數;

由於需要實現在批量讀取完成後回調的功能,例如提示操作完成,統計結果等,我們需要在每個循環中判斷是否讀取到了最後一個
文件,採用的方法是計數器。儘管它在NodeJS單線程環境下,不會造成由於多線程造成的變量讀取衝突,但這種方法顯然是醜陋且繁雜的。

在這裏,我們引入thenjs庫來實現批量異步操作的順序和無序執行以及回調。

這一節我們將用網絡請求代替文件讀取來模擬批量的異步操作。

我們用Rap來模擬http請求並得到自定義數據:
利用Rap模擬JSON數據

JSON數據

對於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的鏈式調用。它的主要特徵如下:

  1. 可以像標準的 Promise 那樣,把N多異步回調函數寫成一個長長的 then 鏈,並且比 Promise 更簡潔自然。因爲如果使用標準 Promise 的 then 鏈,其中的異步函數都必須轉換成 Promise,Thenjs 則無需轉換,像使用 callback
    一樣執行異步函數即可。

  2. 可以像 async那樣實現同步或異步隊列函數,並且比 async 更方便。因爲 async 的隊列是一個個獨立體,而 Thenjs 的隊列在 Thenjs 鏈上,可形成鏈式調用。

  3. 強大的 Error 機制,可以捕捉任何同步或異步的異常錯誤,甚至是位於異步函數中的語法錯誤。並且捕捉的錯誤任君處置。

  4. 開啓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)

eacheachSeries函數中使用了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請求爲例進行了實現並講解,希望我的經驗能幫助到大家。

由於筆者理工科出生,且這是我第一次發技術類博客,可能語言文字組織的不夠嚴密,望各位博友多提寶貴意見,對於文章中出現的錯誤,也請各位斧正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章