Node.js-使用 eventproxy 控制并發(fā)

2018-09-28 22:47 更新

使用 eventproxy 控制并發(fā)

目標(biāo)

建立一個(gè) lesson4 項(xiàng)目,在其中編寫代碼。

代碼的入口是 app.js,當(dāng)調(diào)用 node app.js 時(shí),它會(huì)輸出 CNode(https://cnodejs.org/ ) 社區(qū)首頁(yè)的所有主題的標(biāo)題,鏈接和第一條評(píng)論,以 json 的格式。

輸出示例:

[
  {
    "title": "【公告】發(fā)招聘帖的同學(xué)留意一下這里",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
    "comment1": "呵呵呵呵"
  },
  {
    "title": "發(fā)布一款 Sublime Text 下的 JavaScript 語(yǔ)法高亮插件",
    "href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f",
    "comment1": "沙發(fā)!"
  }
]

挑戰(zhàn)

以上文目標(biāo)為基礎(chǔ),輸出 comment1 的作者,以及他在 cnode 社區(qū)的積分值。

示例:

[
  {
    "title": "【公告】發(fā)招聘帖的同學(xué)留意一下這里",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
    "comment1": "呵呵呵呵",
    "author1": "auser",
    "score1": 80
  },
  ...
]

知識(shí)點(diǎn)

  1. 體會(huì) Node.js 的 callback hell 之美
  2. 學(xué)習(xí)使用 eventproxy 這一利器控制并發(fā)

課程內(nèi)容

這一章我們來(lái)到了 Node.js 最牛逼的地方——異步并發(fā)的內(nèi)容了。

上一課我們介紹了如何使用 superagent 和 cheerio 來(lái)取主頁(yè)內(nèi)容,那只需要發(fā)起一次 http get 請(qǐng)求就能辦到。但這次,我們需要取出每個(gè)主題的第一條評(píng)論,這就要求我們對(duì)每個(gè)主題的鏈接發(fā)起請(qǐng)求,并用 cheerio 去取出其中的第一條評(píng)論。

CNode 目前每一頁(yè)有 40 個(gè)主題,于是我們就需要發(fā)起 1 + 40 個(gè)請(qǐng)求,來(lái)達(dá)到我們這一課的目標(biāo)。

后者的 40 個(gè)請(qǐng)求,我們并發(fā)地發(fā)起:),而且不會(huì)遇到多線程啊鎖什么的,Node.js 的并發(fā)模型跟多線程不同,拋卻那些觀念。更具體一點(diǎn)的話,比如異步到底為何異步,Node.js 為何單線程卻能并發(fā)這類走近科學(xué)的問題,我就不打算講了。對(duì)于這方面有興趣的同學(xué),強(qiáng)烈推薦 @樸靈 的 《九淺一深Node.js》: http://book.douban.com/subject/25768396/ 。

有些逼格比較高的朋友可能聽說(shuō)過 promise 和 generator 這類概念。不過我呢,只會(huì)講 callback,主要原因是我個(gè)人只喜歡 callback。

這次課程我們需要用到三個(gè)庫(kù):superagent cheerio eventproxy(https://github.com/JacksonTian/eventproxy )

手腳架的工作各位自己來(lái),我們一步一步來(lái)一起寫出這個(gè)程序。

首先 app.js 應(yīng)該長(zhǎng)這樣

var eventproxy = require('eventproxy');
var superagent = require('superagent');
var cheerio = require('cheerio');
// url 模塊是 Node.js 標(biāo)準(zhǔn)庫(kù)里面的
// http://nodejs.org/api/url.html
var url = require('url');

var cnodeUrl = 'https://cnodejs.org/';

superagent.get(cnodeUrl)
  .end(function (err, res) {
    if (err) {
      return console.error(err);
    }
    var topicUrls = [];
    var $ = cheerio.load(res.text);
    // 獲取首頁(yè)所有的鏈接
    $('#topic_list .topic_title').each(function (idx, element) {
      var $element = $(element);
      // $element.attr('href') 本來(lái)的樣子是 /topic/542acd7d5d28233425538b04
      // 我們用 url.resolve 來(lái)自動(dòng)推斷出完整 url,變成
      // https://cnodejs.org/topic/542acd7d5d28233425538b04 的形式
      // 具體請(qǐng)看 http://nodejs.org/api/url.html#url_url_resolve_from_to 的示例
      var href = url.resolve(cnodeUrl, $element.attr('href'));
      topicUrls.push(href);
    });

    console.log(topicUrls);
  });

運(yùn)行 node app.js

輸出如下圖:

OK,這時(shí)候我們已經(jīng)得到所有 url 的地址了,接下來(lái),我們把這些地址都抓取一遍,就完成了,Node.js 就是這么簡(jiǎn)單。

抓取之前,還是得介紹一下 eventproxy 這個(gè)庫(kù)。

用 js 寫過異步的同學(xué)應(yīng)該都知道,如果你要并發(fā)異步獲取兩三個(gè)地址的數(shù)據(jù),并且要在獲取到數(shù)據(jù)之后,對(duì)這些數(shù)據(jù)一起進(jìn)行利用的話,常規(guī)的寫法是自己維護(hù)一個(gè)計(jì)數(shù)器。

先定義一個(gè) var count = 0,然后每次抓取成功以后,就 count++。如果你是要抓取三個(gè)源的數(shù)據(jù),由于你根本不知道這些異步操作到底誰(shuí)先完成,那么每次當(dāng)抓取成功的時(shí)候,就判斷一下 count === 3。當(dāng)值為真時(shí),使用另一個(gè)函數(shù)繼續(xù)完成操作。

而 eventproxy 就起到了這個(gè)計(jì)數(shù)器的作用,它來(lái)幫你管理到底這些異步操作是否完成,完成之后,它會(huì)自動(dòng)調(diào)用你提供的處理函數(shù),并將抓取到的數(shù)據(jù)當(dāng)參數(shù)傳過來(lái)。

假設(shè)我們不使用 eventproxy 也不使用計(jì)數(shù)器時(shí),抓取三個(gè)源的寫法是這樣的:

// 參考 jquery 的 $.get 的方法
$.get("http://data1_source", function (data1) {
  // something
  $.get("http://data2_source", function (data2) {
    // something
    $.get("http://data3_source", function (data3) {
      // something
      var html = fuck(data1, data2, data3);
      render(html);
    });
  });
});

上述的代碼大家都寫過吧。先獲取 data1,獲取完成之后獲取 data2,然后再獲取 data3,然后 fuck 它們,進(jìn)行輸出。

但大家應(yīng)該也想到了,其實(shí)這三個(gè)源的數(shù)據(jù),是可以并行去獲取的,data2 的獲取并不依賴 data1 的完成,data3 同理也不依賴 data2。

于是我們用計(jì)數(shù)器來(lái)寫,會(huì)寫成這樣:

(function () {
  var count = 0;
  var result = {};

  $.get('http://data1_source', function (data) {
    result.data1 = data;
    count++;
    handle();
    });
  $.get('http://data2_source', function (data) {
    result.data2 = data;
    count++;
    handle();
    });
  $.get('http://data3_source', function (data) {
    result.data3 = data;
    count++;
    handle();
    });

  function handle() {
    if (count === 3) {
      var html = fuck(result.data1, result.data2, result.data3);
      render(html);
    }
  }
})();

丑的一逼,也不算丑,主要我寫代碼好看。

如果我們用 eventproxy,寫出來(lái)是這樣的:

var ep = new eventproxy();
ep.all('data1_event', 'data2_event', 'data3_event', function (data1, data2, data3) {
  var html = fuck(data1, data2, data3);
  render(html);
});

$.get('http://data1_source', function (data) {
  ep.emit('data1_event', data);
  });

$.get('http://data2_source', function (data) {
  ep.emit('data2_event', data);
  });

$.get('http://data3_source', function (data) {
  ep.emit('data3_event', data);
  });

好看多了是吧,也就是個(gè)高等計(jì)數(shù)器嘛。

ep.all('data1_event', 'data2_event', 'data3_event', function (data1, data2, data3) {});

這一句,監(jiān)聽了三個(gè)事件,分別是 data1_event, data2_event, data3_event,每次當(dāng)一個(gè)源的數(shù)據(jù)抓取完成時(shí),就通過 ep.emit() 來(lái)告訴 ep 自己,某某事件已經(jīng)完成了。

當(dāng)三個(gè)事件未同時(shí)完成時(shí),ep.emit() 調(diào)用之后不會(huì)做任何事;當(dāng)三個(gè)事件都完成的時(shí)候,就會(huì)調(diào)用末尾的那個(gè)回調(diào)函數(shù),來(lái)對(duì)它們進(jìn)行統(tǒng)一處理。

eventproxy 提供了不少其他場(chǎng)景所需的 API,但最最常用的用法就是以上的這種,即:

  1. var ep = new eventproxy(); 得到一個(gè) eventproxy 實(shí)例。
  2. 告訴它你要監(jiān)聽哪些事件,并給它一個(gè)回調(diào)函數(shù)。ep.all('event1', 'event2', function (result1, result2) {})
  3. 在適當(dāng)?shù)臅r(shí)候 ep.emit('event_name', eventData)。

eventproxy 這套處理異步并發(fā)的思路,我一直覺得就像是匯編里面的 goto 語(yǔ)句一樣,程序邏輯在代碼中隨處跳躍。本來(lái)代碼已經(jīng)執(zhí)行到 100 行了,突然 80 行的那個(gè)回調(diào)函數(shù)又開始工作了。如果你異步邏輯復(fù)雜點(diǎn)的話,80 行的這個(gè)函數(shù)完成之后,又激活了 60 行的另外一個(gè)函數(shù)。并發(fā)和嵌套的問題雖然解決了,但老祖宗們消滅了幾十年的 goto 語(yǔ)句又回來(lái)了。

至于這套思想糟糕不糟糕,我個(gè)人倒是覺得還是不糟糕,用熟了看起來(lái)蠻清晰的。不過 js 這門渣渣語(yǔ)言本來(lái)就亂嘛,什么變量提升(http://www.cnblogs.com/damonlan/archive/2012/07/01/2553425.html )啊,沒有 main 函數(shù)啊,變量作用域啊,數(shù)據(jù)類型常常簡(jiǎn)單得只有數(shù)字、字符串、哈希、數(shù)組啊,這一系列的問題,都不是事兒。

編程語(yǔ)言美丑啥的,咱心中有佛就好。

回到正題,之前我們已經(jīng)得到了一個(gè)長(zhǎng)度為 40 的 topicUrls 數(shù)組,里面包含了每條主題的鏈接。那么意味著,我們接下來(lái)要發(fā)出 40 個(gè)并發(fā)請(qǐng)求。我們需要用到 eventproxy 的 #after API。

大家自行學(xué)習(xí)一下這個(gè) API 吧:https://github.com/JacksonTian/eventproxy#%E9%87%8D%E5%A4%8D%E5%BC%82%E6%AD%A5%E5%8D%8F%E4%BD%9C

我代碼就直接貼了哈。

// 得到 topicUrls 之后

// 得到一個(gè) eventproxy 的實(shí)例
var ep = new eventproxy();

// 命令 ep 重復(fù)監(jiān)聽 topicUrls.length 次(在這里也就是 40 次) `topic_html` 事件再行動(dòng)
ep.after('topic_html', topicUrls.length, function (topics) {
  // topics 是個(gè)數(shù)組,包含了 40 次 ep.emit('topic_html', pair) 中的那 40 個(gè) pair

  // 開始行動(dòng)
  topics = topics.map(function (topicPair) {
    // 接下來(lái)都是 jquery 的用法了
    var topicUrl = topicPair[0];
    var topicHtml = topicPair[1];
    var $ = cheerio.load(topicHtml);
    return ({
      title: $('.topic_full_title').text().trim(),
      href: topicUrl,
      comment1: $('.reply_content').eq(0).text().trim(),
    });
  });

  console.log('final:');
  console.log(topics);
});

topicUrls.forEach(function (topicUrl) {
  superagent.get(topicUrl)
    .end(function (err, res) {
      console.log('fetch ' + topicUrl + ' successful');
      ep.emit('topic_html', [topicUrl, res.text]);
    });
});

輸出長(zhǎng)這樣:

完整的代碼請(qǐng)查看 lesson4 目錄下的 app.js 文件

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)