前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket

# 使用 async 控制并发

30 秒速记

  • 并发控制 = 同时在跑的任务不超过 N 个,一个结束再补一个,不是把数组全丢给 Promise.all
  • async 库核心接口:mapLimit(arr, limit, iterator, cb) 处理固定数组,queue(worker, concurrency) 处理持续入队
  • 失败策略要先定:一错全停、收集全部错误、还是重试;别静默吞掉
  • 重试只给幂等请求,用指数退避加随机抖动,再设总次数上限
  • 现在新代码多用 Promise 版:p-limit 或手写任务池,思路和 mapLimit 一样

控制并发就是给异步任务加个闸:同一时间最多跑 N 个,跑完一个再放一个进来。 爬虫、批量上传、批量调接口都会用到,一下子发几百个请求,要么被对方封 IP,要么把自己的连接数、内存打满。async 库的 mapLimit 是早期最常用的写法,现在用 Promise 的话 p-limit 一行就能搞定,或者自己写个任务池。不管用哪个,重点是想清楚某个任务失败了整体怎么办,是马上停还是继续跑完再汇总错误。

理解「使用 async 控制并发」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 event-loop 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

「并发」和「并发控制」常被混着说。Node 里发起异步请求本身就是并发的,问题在于不加限制时会一下子把所有请求都发出去。并发控制要做的是让「同时在途的任务数」保持在一个上限内。

用 Promise 写,最简单的是 p-limit:

import pLimit from 'p-limit';

const limit = pLimit(5);
const urls = Array.from({ length: 30 }, (_, i) => `https://example.com/${i}`);

const results = await Promise.allSettled(
  urls.map((url) => limit(() => fetch(url).then((r) => r.text())))
);
// 任意时刻最多 5 个 fetch 在跑;allSettled 让失败的不影响其他
const failed = results.filter((r) => r.status === 'rejected');
console.log(`成功 ${results.length - failed.length},失败 ${failed.length}`);
@前端进阶之旅: 代码已经复制到剪贴板

注意这里 limit(() => fetch(url)) 包的是函数,真正的 fetch 由 limit 决定什么时候调用;写成 limit(fetch(url)) 就又变成一次全发出去了。

除了上限,真实场景通常还要配三样东西:单个任务的超时(AbortSignal.timeout(5000)),失败的重试策略,以及整体的取消能力,比如用户关掉页面或者任务被手动终止时,用 AbortController 让还没开始的任务不再启动。这些和用 async 还是 Promise 无关,是调度语义本身要回答的问题。

面试官追问

追问 1Promise.all(urls.map(fetch)) 处理 1000 个链接,有什么问题?
参考回答

map 那一刻 1000 个请求就全发出去了,Promise.all 只负责等结果,不限流。要先把任务包成函数,再交给任务池控制同时启动的数量。

追问 2手写一个并发上限为 5 的任务池,核心思路是什么?
参考回答

起 5 个 worker,每个 worker 循环从共享的下标里取下一个任务,跑完再取:

追问 3并发控制后脚本跑完不退出,最后一个回调已经执行了,查什么?
参考回答

事件循环里还有东西挂着:没关的 keep-alive 连接、没清的 setInterval、没关的数据库连接。用 why-is-node-running 或 process._getActiveHandles() 看看是谁。

追问 4某个请求失败了,是让整批停掉,还是继续跑完?
参考回答

看业务。批量导入要求全量一致就一错全停;爬虫、批量通知这类允许部分失败的,继续跑完,把失败项和原因汇总出来,再决定要不要重试。别让它静默变成空结果。

追问 5接口偶尔超时,加了重试后对方服务被打得更挂了,为什么?
参考回答

所有失败请求同时立即重试,等于又来了一波洪峰。重试要退避,比如 1s、2s、4s 再加随机抖动,设最大次数,并且只重试 GET 这类幂等请求。

# 《使用 async 控制并发》

# 目标

30 秒速记

  • 任务:node app.js 输出 CNode 首页每个主题的 title、href、comment1,格式是 JSON
  • 和上一节的区别只有一个:同时发出的请求不能超过 5 个
  • 流程:抓首页拿主题链接 → 限流抓每个详情页 → 解析第一条评论 → 按首页顺序组装输出
  • 没评论的主题要有明确约定(比如 comment1: null),别凭空编一个

这一节的需求和上一节一样,抓首页主题和第一条评论,唯一的新约束是并发连接数最多 5 个。 上一节是解析完首页就把四十个详情页请求一次性发出去,这一节要换成排队执行:始终最多 5 个在跑,有一个完成才补下一个。所以代码结构上,抓首页和解析不变,变的只是「怎么发起详情页请求」这一层。顺带要处理没有评论的主题,输出里给个 null,不要让整个对象缺字段。

建立一个 lesson5 项目,在其中编写代码。

代码的入口是 app.js,当调用 node app.js 时,它会输出 CNode( https://cnodejs.org/ ) 社区首页的所有主题的标题,链接和第一条评论,以 json 的格式。

注意:与上节课不同,并发连接数需要控制在 5 个。

输出示例:

[
  {
    "title": "【公告】发招聘帖的同学留意一下这里",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
    "comment1": "呵呵呵呵"
  },
  {
    "title": "发布一款 Sublime Text 下的 JavaScript 语法高亮插件",
    "href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f",
    "comment1": "沙发!"
  }
]
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1怎么证明并发真的没超过 5?只看最终 JSON 行吗?
参考回答

不行,最终结果和并发数没关系。在请求开始时计数加一、结束时减一,并记录最大值,跑完断言最大值不超过 5。失败分支也要减,不然计数会越来越大。

追问 2主题从 40 个变成 1000 个,并发上限要不要跟着调大?
参考回答

不用。任务数和并发数是两个参数,任务多了只是排队时间变长。并发上限取决于对方服务能扛多少,而不是你有多少活。

追问 3跑完输出空数组,退出码还是 0,先查哪一段?
参考回答

先查首页解析出了几个链接,多半是页面结构变了,选择器没匹配到,后面自然什么都没跑。确定有链接后再看详情任务有没有被提交、最终回调是不是在任务完成前就输出了。

追问 4有个主题没有评论,解析时报错导致整个程序崩了,怎么改?
参考回答

取第一条评论前先判断存不存在,没有就给 null。cheerio 选不到元素时 .text() 返回空字符串不会报错,崩通常是对 undefined 继续取属性,比如直接用了 res.text 而请求其实失败了。

# 知识点

30 秒速记

  • 学会用 async 库的接口控制并发数,最常用的是 mapLimit 和 queue
  • 只 require('async') 不会限流,必须用带 Limit 或 concurrency 参数的接口
  • mapLimit:固定数组 → 结果数组,顺序和输入一致;queue:任务可以随时 push
  • async v3 支持传 async 函数,省略回调时返回 Promise,可以直接 await
  • 每个任务的回调只能调一次,调两次会让结果和计数全乱

这一节要会的就是用 async 库把一批异步任务的同时执行数限制住。 最常用的是 mapLimit,传入数组、上限、处理函数,它会保证同时最多跑上限个,结果按输入顺序放好。另一个是 queue,适合任务不是一开始就确定、而是陆续进来的情况。新版本 async 已经支持 async 函数和 Promise,写起来不一定非得回调;但不管哪种写法,每个任务只能完成一次,这个约定被打破,库也救不了。

  1. 学习 async( https://github.com/caolan/async ) 的使用。这里有个详细的 async demo 演示: https://github.com/alsotang/async_demo
  2. 学习使用 async 来控制并发连接数。

面试官追问

追问 1代码里引入了 async,但还是 urls.forEach(url => fetch(url)),算限流吗?
参考回答

不算。限流靠的是调度接口,不是装了这个包。要把请求放进 async.mapLimit(urls, 5, ...) 的处理函数里,由它决定什么时候调用。

追问 2
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket
完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

使用 async 控制并发|Nodejs专题系列

# 使用 async 控制并发

30 秒速记

  • 并发控制 = 同时在跑的任务不超过 N 个,一个结束再补一个,不是把数组全丢给 Promise.all
  • async 库核心接口:mapLimit(arr, limit, iterator, cb) 处理固定数组,queue(worker, concurrency) 处理持续入队
  • 失败策略要先定:一错全停、收集全部错误、还是重试;别静默吞掉
  • 重试只给幂等请求,用指数退避加随机抖动,再设总次数上限
  • 现在新代码多用 Promise 版:p-limit 或手写任务池,思路和 mapLimit 一样

控制并发就是给异步任务加个闸:同一时间最多跑 N 个,跑完一个再放一个进来。 爬虫、批量上传、批量调接口都会用到,一下子发几百个请求,要么被对方封 IP,要么把自己的连接数、内存打满。async 库的 mapLimit 是早期最常用的写法,现在用 Promise 的话 p-limit 一行就能搞定,或者自己写个任务池。不管用哪个,重点是想清楚某个任务失败了整体怎么办,是马上停还是继续跑完再汇总错误。

理解「使用 async 控制并发」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 event-loop 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

「并发」和「并发控制」常被混着说。Node 里发起异步请求本身就是并发的,问题在于不加限制时会一下子把所有请求都发出去。并发控制要做的是让「同时在途的任务数」保持在一个上限内。

用 Promise 写,最简单的是 p-limit:

import pLimit from 'p-limit';

const limit = pLimit(5);
const urls = Array.from({ length: 30 }, (_, i) => `https://example.com/${i}`);

const results = await Promise.allSettled(
  urls.map((url) => limit(() => fetch(url).then((r) => r.text())))
);
// 任意时刻最多 5 个 fetch 在跑;allSettled 让失败的不影响其他
const failed = results.filter((r) => r.status === 'rejected');
console.log(`成功 ${results.length - failed.length},失败 ${failed.length}`);
@前端进阶之旅: 代码已经复制到剪贴板

注意这里 limit(() => fetch(url)) 包的是函数,真正的 fetch 由 limit 决定什么时候调用;写成 limit(fetch(url)) 就又变成一次全发出去了。

除了上限,真实场景通常还要配三样东西:单个任务的超时(AbortSignal.timeout(5000)),失败的重试策略,以及整体的取消能力,比如用户关掉页面或者任务被手动终止时,用 AbortController 让还没开始的任务不再启动。这些和用 async 还是 Promise 无关,是调度语义本身要回答的问题。

面试官追问

追问 1Promise.all(urls.map(fetch)) 处理 1000 个链接,有什么问题?
参考回答

map 那一刻 1000 个请求就全发出去了,Promise.all 只负责等结果,不限流。要先把任务包成函数,再交给任务池控制同时启动的数量。

追问 2手写一个并发上限为 5 的任务池,核心思路是什么?
参考回答

起 5 个 worker,每个 worker 循环从共享的下标里取下一个任务,跑完再取:

追问 3并发控制后脚本跑完不退出,最后一个回调已经执行了,查什么?
参考回答

事件循环里还有东西挂着:没关的 keep-alive 连接、没清的 setInterval、没关的数据库连接。用 why-is-node-running 或 process._getActiveHandles() 看看是谁。

追问 4某个请求失败了,是让整批停掉,还是继续跑完?
参考回答

看业务。批量导入要求全量一致就一错全停;爬虫、批量通知这类允许部分失败的,继续跑完,把失败项和原因汇总出来,再决定要不要重试。别让它静默变成空结果。

追问 5接口偶尔超时,加了重试后对方服务被打得更挂了,为什么?
参考回答

所有失败请求同时立即重试,等于又来了一波洪峰。重试要退避,比如 1s、2s、4s 再加随机抖动,设最大次数,并且只重试 GET 这类幂等请求。

# 《使用 async 控制并发》

# 目标

30 秒速记

  • 任务:node app.js 输出 CNode 首页每个主题的 title、href、comment1,格式是 JSON
  • 和上一节的区别只有一个:同时发出的请求不能超过 5 个
  • 流程:抓首页拿主题链接 → 限流抓每个详情页 → 解析第一条评论 → 按首页顺序组装输出
  • 没评论的主题要有明确约定(比如 comment1: null),别凭空编一个

这一节的需求和上一节一样,抓首页主题和第一条评论,唯一的新约束是并发连接数最多 5 个。 上一节是解析完首页就把四十个详情页请求一次性发出去,这一节要换成排队执行:始终最多 5 个在跑,有一个完成才补下一个。所以代码结构上,抓首页和解析不变,变的只是「怎么发起详情页请求」这一层。顺带要处理没有评论的主题,输出里给个 null,不要让整个对象缺字段。

建立一个 lesson5 项目,在其中编写代码。

代码的入口是 app.js,当调用 node app.js 时,它会输出 CNode( https://cnodejs.org/ ) 社区首页的所有主题的标题,链接和第一条评论,以 json 的格式。

注意:与上节课不同,并发连接数需要控制在 5 个。

输出示例:

[
  {
    "title": "【公告】发招聘帖的同学留意一下这里",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
    "comment1": "呵呵呵呵"
  },
  {
    "title": "发布一款 Sublime Text 下的 JavaScript 语法高亮插件",
    "href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f",
    "comment1": "沙发!"
  }
]
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1怎么证明并发真的没超过 5?只看最终 JSON 行吗?
参考回答

不行,最终结果和并发数没关系。在请求开始时计数加一、结束时减一,并记录最大值,跑完断言最大值不超过 5。失败分支也要减,不然计数会越来越大。

追问 2主题从 40 个变成 1000 个,并发上限要不要跟着调大?
参考回答

不用。任务数和并发数是两个参数,任务多了只是排队时间变长。并发上限取决于对方服务能扛多少,而不是你有多少活。

追问 3跑完输出空数组,退出码还是 0,先查哪一段?
参考回答

先查首页解析出了几个链接,多半是页面结构变了,选择器没匹配到,后面自然什么都没跑。确定有链接后再看详情任务有没有被提交、最终回调是不是在任务完成前就输出了。

追问 4有个主题没有评论,解析时报错导致整个程序崩了,怎么改?
参考回答

取第一条评论前先判断存不存在,没有就给 null。cheerio 选不到元素时 .text() 返回空字符串不会报错,崩通常是对 undefined 继续取属性,比如直接用了 res.text 而请求其实失败了。

# 知识点

30 秒速记

  • 学会用 async 库的接口控制并发数,最常用的是 mapLimit 和 queue
  • 只 require('async') 不会限流,必须用带 Limit 或 concurrency 参数的接口
  • mapLimit:固定数组 → 结果数组,顺序和输入一致;queue:任务可以随时 push
  • async v3 支持传 async 函数,省略回调时返回 Promise,可以直接 await
  • 每个任务的回调只能调一次,调两次会让结果和计数全乱

这一节要会的就是用 async 库把一批异步任务的同时执行数限制住。 最常用的是 mapLimit,传入数组、上限、处理函数,它会保证同时最多跑上限个,结果按输入顺序放好。另一个是 queue,适合任务不是一开始就确定、而是陆续进来的情况。新版本 async 已经支持 async 函数和 Promise,写起来不一定非得回调;但不管哪种写法,每个任务只能完成一次,这个约定被打破,库也救不了。

  1. 学习 async( https://github.com/caolan/async ) 的使用。这里有个详细的 async demo 演示: https://github.com/alsotang/async_demo
  2. 学习使用 async 来控制并发连接数。

面试官追问

追问 1代码里引入了 async,但还是 urls.forEach(url => fetch(url)),算限流吗?
参考回答

不算。限流靠的是调度接口,不是装了这个包。要把请求放进 async.mapLimit(urls, 5, ...) 的处理函数里,由它决定什么时候调用。

追问 2固定的链接数组要收集结果,用 mapLimit 还是 queue?
参考回答

用 mapLimit,输入和输出一一对应,最后回调直接拿到结果数组。queue 适合任务持续进来、多个生产者往里推的场景,用它处理固定数组还得自己收集结果和判断全部完成。

追问 3日志里并发计数出现负数,最终回调还执行了两次,问题在哪?
参考回答

某个任务的回调被调了两次,常见是成功分支调了一次,catch 里又调了一次,或者回调后没 return 继续往下走。每个分支调完回调就 return。

追问 4现在写新代码,async 库还值得用吗?
参考回答

老项目里有就继续用,没必要为换而换。新代码我更倾向 p-limit 或手写任务池,直接配合 async/await,少一个回调风格的依赖。

# 课程内容

30 秒速记

  • async.mapLimit(urls, 5, iterator, callback):同时最多 5 个在跑,完成一个立刻补一个
  • 不是「每批 5 个、等一批跑完再下一批」,是滑动窗口,活动数一直顶在 5
  • 结果数组按输入顺序排列,和完成先后无关;任一任务出错,最终回调立刻收到 err
  • 选型:少量不同数据源汇总用 eventproxy;同类任务要排队、限并发用 async
  • 持续有任务进来用 queue(worker, concurrency)

mapLimit 的效果是一个滑动窗口:开始一口气启动 5 个,之后谁先完成谁就让出一个位置,下一个任务马上补进来。 所以日志里能看到并发数从 1 涨到 5,然后一直稳定在 5,直到最后任务不够了才降下来。它不是分批,分批会被最慢的那个拖住,滑动窗口吞吐更高。最终回调拿到的结果数组和输入顺序一一对应,不用自己按 URL 去对。要注意出错的行为:某个任务回调里传了 err,最终回调会立刻被调用,其他还在跑的任务不会被取消,只是结果被丢掉了。

← node8_napi使用 eventproxy 控制并发 →

fe