Node.js实战:利用eventproxy模块高效控制异步并发

作者:慢半拍时间:2026-10-29 16:10:37浏览:339

本文深入解析Node.js中的异步并发处理,重点介绍eventproxy模块如何简化复杂的回调地狱。通过对比传统计数器方式,展示如何利用eventproxy监听多个异步事件,实现高效、清晰的数据抓取与处理流程。

Node.js实战:利用eventproxy模块高效控制异步并发

目标

创建一个名为 lesson4 的项目,并在其中编写相关代码。

项目的入口文件为 app.js。执行 node app.js 命令后,程序将输出 CNode(https://cnodejs.org/ ) 社区首页所有主题的标题、链接以及第一条评论,并以 JSON 格式呈现。

输出示例:


[
 {
  "title": "【公告】发招聘帖的同学留意一下这里",
  "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
  "comment1": "呵呵呵呵"
 },
 {
  "title": "发布一款 Sublime Text 下的 JavaScript 语法高亮插件",
  "href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f",
  "comment1": "沙发!"
 }
]

挑战

基于上述目标,进一步输出 comment1 的作者信息,以及该作者在 cnode 社区的积分值。

示例:


[
 {
  "title": "【公告】发招聘帖的同学留意一下这里",
  "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
  "comment1": "呵呵呵呵",
  "author1": "auser",
  "score1": 80
 },
 ...
]

知识点与课程目标

知识点

深入体会 Node.js 中 callback hell(回调地狱)的特点。

学习使用 eventproxy 这一工具来有效控制并发操作。

课程内容

本章我们将进入 Node.js 最核心的部分——异步并发处理。

上一课我们介绍了如何使用 superagent 和 cheerio 获取主页内容,这仅需发起一次 HTTP GET 请求即可完成。然而,本课的目标是获取每个主题的第一条评论,这意味着我们需要对每个主题的链接单独发起请求,并利用 cheerio 解析提取其中的第一条评论。

CNode 社区每页展示 40 个主题,因此为了达成本课目标,我们需要发起 1 + 40 个请求。

这后续的 40 个请求我们将并发发起:),且无需担心多线程或锁的问题,因为 Node.js 的并发模型与多线程不同,请摒弃相关固有观念。具体而言,关于异步的原理、Node.js 为何单线程却能并发等深入问题,此处不再赘述。对此感兴趣的同学,强烈推荐朴灵的《九浅一深Node.js》: http://book.douban.com/subject/25768396/ 。

部分资深开发者可能听说过 promise 和 generator 等概念。但本课程仅讲解 callback,主要因为个人偏好 callback 风格。

项目环境与依赖库

本次课程需要用到三个库:superagent、cheerio 和 eventproxy(https://github.com/JacksonTian/eventproxy )。 环境搭建工作由各位自行完成,我们将逐步编写该程序。

首先,app.js 的代码结构大致如下:


var eventproxy = require('eventproxy');
var superagent = require('superagent');
var cheerio = require('cheerio');
// url 模块是 Node.js 标准库里面的
// 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);
  // 获取首页所有的链接
  $('#topic_list .topic_title').each(function (idx, element) {
   var $element = $(element);
   // $element.attr('href') 本来的样子是 /topic/542acd7d5d28233425538b04
   // 我们用 url.resolve 来自动推断出完整 url,变成
   // https://cnodejs.org/topic/542acd7d5d28233425538b04 的形式
   // 具体请看 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);
 });

运行 node app.js

输出结果如下图所示:

Node.js实战:利用eventproxy模块高效控制异步并发

eventproxy模块详解

此时,我们已经获取了所有 URL 地址。接下来,只需抓取这些地址的内容即可完成目标,Node.js 的处理方式确实简洁。 在抓取之前,先介绍 eventproxy 这个库。

使用 JavaScript 编写异步代码的开发者应该知道,如果要并发异步获取两三个地址的数据,并在获取后统一利用这些数据,常规做法是手动维护一个计数器。

首先定义 var count = 0,每次抓取成功后执行 count++。若需抓取三个源的数据,由于异步操作完成顺序不确定,每次抓取成功后判断 count === 3。当值为真时,调用另一个函数继续完成后续操作。 而 eventproxy 就起到了这个计数器的作用,它负责管理异步操作是否全部完成,完成后自动调用提供的处理函数,并将抓取到的数据作为参数传入。 假设不使用 eventproxy 也不使用计数器时,抓取三个源的写法如下:

// 参考 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,然后处理它们并输出。

但大家应该意识到,这三个源的数据可以并行获取,data2 的获取不依赖 data1 的完成,data3 同理也不依赖 data2。

因此,使用计数器编写的代码大致如下:


(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,代码将写成这样:


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

看起来清晰多了,本质上就是一个高级计数器。


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

这一行代码监听了三个事件,分别是 data1_event、data2_event 和 data3_event。每当一个源的数据抓取完成,便通过 ep.emit() 通知 ep 该事件已完成。

当三个事件未全部完成时,ep.emit() 调用后不会执行任何操作;当三个事件均完成后,将调用末尾的回调函数,对数据进行统一处理。

eventproxy 提供了多种适用于其他场景的 API,但最常用用法如下:

首先 var ep = new eventproxy(); 创建一个 eventproxy 实例。

接着告诉它需要监听哪些事件,并传入一个回调函数。ep.all('event1', 'event2', function (result1, result2) {})。 在适当时机调用 ep.emit('event_name', eventData)。

eventproxy 处理异步并发的思路,我一直觉得类似于汇编中的 goto 语句,程序逻辑在代码中跳跃。代码已执行到第 100 行,突然第 80 行的回调函数开始工作。如果异步逻辑复杂,第 80 行的函数完成后,又激活第 60 行的另一个函数。虽然解决了并发和嵌套问题,但几十年前被消灭的 goto 语句似乎又回来了。

至于这种思想是否糟糕,我个人认为并不糟糕,熟练后看起来相当清晰。不过 JavaScript 这门语言本身较为混乱,存在变量提升( http://www.cnblogs.com/damonlan/archive/2012/07/01/2553425.html )、没有 main 函数、变量作用域等问题,数据类型也简单,仅有数字、字符串、哈希、数组等,这些都不是问题。 编程语言的美丑,心中自有标准。

使用after API处理并发请求

回到正题,之前我们得到了一个长度为 40 的 topicUrls 数组,包含每条主题的链接。这意味着接下来要发出 40 个并发请求。我们需要用到 eventproxy 的 #after API。

请大家自行学习该 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 之后
// 得到一个 eventproxy 的实例
var ep = new eventproxy();
// 命令 ep 重复监听 topicUrls.length 次(在这里也就是 40 次) `topic_html` 事件再行动
ep.after('topic_html', topicUrls.length, function (topics) {
 // topics 是个数组,包含了 40 次 ep.emit('topic_html', pair) 中的那 40 个 pair
 // 开始行动
 topics = topics.map(function (topicPair) {
  // 接下来都是 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]);
  });
});

输出结果如下:

Node.js实战:利用eventproxy模块高效控制异步并发

完整代码请查看 lesson4 目录下的 app.js 文件

总结

总结

今天介绍的 eventproxy 模块用于控制并发。有时我们需要同时发送 N 个 HTTP 请求,然后利用获取的数据进行后期处理。如何方便地判断数据已全部并发获取,即可使用该模块。此外,该模块不仅可在服务端使用,也可应用于客户端。

作者最新文章