Node.js 异步编程入门:Callback 模式实战解析
本文深入解析 Node.js 异步编程中的 Callback 模式。通过一个获取多个 IP 对应天气信息的完整案例,演示如何结合 fs、request 和 querystring 模块处理并行异步请求。文章详细拆解了回调函数的嵌套逻辑、错误处理机制以及 remain 计数器在并行控制中的应用,揭示 Callback 模式在复杂异步场景下的挑战。
Node.js 异步编程概述
Node.js 基于 V8 JavaScript 引擎构建,采用单线程架构。为了处理可能造成阻塞的 I/O 操作,Node.js 采用了与常规 Web JavaScript 不同的异步编程模型。在 Node.js 环境中,文件读取、数据库访问及网络请求等操作通常都是异步执行的。对于 Node.js 初学者或从其他编程语言迁移过来的开发者而言,理解异步编程往往是一个难点。本章将循序渐进地讲解 Node.js 异步编程的各个层面,从基础的 Callback 模式,逐步深入到 Thunk、Promise、co 库,直至 ES7 计划中的 async/await 语法。
首先,我们通过一个具体的异步编程实例来展开讨论。
实战案例:IP 天气查询
获取多个 IP 地址所在地的天气信息
在 ip.json 文件中,我们存储了一个包含多个 IP 地址的数组,这些 IP 来自不同地区的访问者,具体内容如下:
代码如下:
// ip.json
["115.29.230.208", "180.153.132.38", "74.125.235.224", "91.239.201.98", "60.28.215.115"]我们的目标是获取每个 IP 地址所在地的当前天气状况,并将结果输出到 weather.json 文件中,格式如下:
代码如下:
// weather.json
[
{ "ip": "115.29.230.208", "weather": "Clouds", "region": "Zhejiang" },
{ "ip": "180.153.132.38", "weather": "Clear", "region": "Shanghai" },
{ "ip": "74.125.235.224", "weather": "Rain", "region": "California" },
{ "ip": "60.28.215.115", "weather": "Clear", "region": "Tianjin" }
]
梳理整体思路,我们可以将任务分解为以下几个步骤:
1. 读取 IP 地址列表; 2. 根据 IP 地址查询其所在的地理位置; 3. 根据地理位置信息查询当地的天气数据; 4. 将最终结果写入 weather.json 文件。
上述所有步骤均设计为异步操作(尽管文件读写支持同步模式,但为了示例统一性,此处全部采用异步方式)。
Callback 模式实现
Callback 模式
首先,我们尝试不依赖任何第三方库,仅使用 Node.js API 通常提供的方式——即传递一个 callback 函数作为异步回调——来实现这一功能。我们将借助三个基础模块:
1. fs:用于从 ip.json 文件读取 IP 列表,并将最终结果写入文件; 2. request:用于发送 HTTP 请求,根据 IP 地址获取地理位置(geo)数据,进而通过 geo 数据获取天气信息; 3. querystring:用于组装发送请求所需的 URL 参数。
新建一个名为 callback.js 的文件,并引入上述模块:
代码如下:
// callback.js
var fs = require('fs')
var request = require('request')
var qs = require('querystring')
首先读取文件中的 IP 列表,调用 fs.readFile 读取文件内容,并通过 JSON.parse 解析 JSON 数据:
代码如下:
...
function readIP(path, callback) {
fs.readFile(path, function(err, data) {
if (err) {
callback(err)
} else {
try {
data = JSON.parse(data)
callback(null, data)
} catch (error) {
callback(error)
}
}
})
}
...
接着,利用获取到的 IP 查询地理位置(geo)。我们使用 request 模块请求一个公开的 geo 服务:
代码如下:
...
function ip2geo(ip, callback) {
var url = 'http://www.telize.com/geoip/' + ip
request({
url: url,
json: true
}, function(err, resp, body) {
callback(err, body)
})
}
...
利用 geo 数据进一步查询天气信息:
代码如下:
...
function geo2weather(lat, lon, callback) {
var params = {
lat: lat,
lon: lon,
APPID: '9bf4d2b07c7ddeb780c5b32e636c679d'
}
var url = 'http://api.openweathermap.org/data/2.5/weather?' + qs.stringify(params)
request({
url: url,
json: true,
}, function(err, resp, body) {
callback(err, body)
})
}
...现在我们已经具备了获取 geo 和 weather 的接口。接下来需要处理一个相对复杂的问题:由于存在多个 IP,我们需要并行读取 geo 数据,并并行查询天气数据:
代码如下:
...
function ips2geos(ips, callback) {
var geos = []
var ip
var remain = ips.length
for (var i = 0; i < ips.length; i++) {
ip = ips[i];
(function(ip) {
ip2geo(ip, function(err, geo) {
if (err) {
callback(err)
} else {
geo.ip = ip
geos.push(geo)
remain--
}
if (remain == 0) {
callback(null, geos)
}
})
})(ip)
}
}
function geos2weathers(geos, callback) {
var weathers = []
var geo
var remain = geos.length
for (var i = 0; i < geos.length; i++) {
geo = geos[i];
(function(geo) {
geo2weather(geo.latitude, geo.longitude, function(err, weather) {
if (err) {
callback(err)
} else {
weather.geo = geo
weathers.push(weather)
remain--
}
if (remain == 0) {
callback(null, weathers)
}
})
})(geo)
}
}
...
ips2geos 和 geos2weathers 函数均使用了一种较为原始的方法,即通过 remain 变量来计算等待返回的请求数量。当 remain 减为 0 时,表示所有并行请求结束,此时将处理结果装入数组并通过回调函数返回。
最后一步是将处理好的结果写入 weather.json 文件:
代码如下:
...
function writeWeather(weathers, callback) {
var output = []
var weather
for (var i = 0; i < weathers.length; i++) {
weather = weathers[i]
output.push({
ip: weather.geo.ip,
weather: weather.weather[0].main,
region: weather.geo.region
})
}
fs.writeFile('./weather.json', JSON.stringify(output, null, ' '), callback)
}
...
组合上述所有函数,即可实现我们的目标:
代码如下:
...
function handlerError(err) {
console.log('error: ' + err)
}
readIP('./ip.json', function(err, ips) {
if (err) {
handlerError(err)
} else {
ips2geos(ips, function(err, geos) {
if (err) {
handlerError(err)
} else {
geos2weathers(geos, function(err, weathers) {
if (err) {
handlerError(err)
} else {
writeWeather(weathers, function(err) {
if (err) {
handlerError(err)
} else {
console.log('success!')
}
})
}
})
}
})
}
})
面对如此深度的嵌套,你可能会认为这就是 JavaScript 异步编程的问题所在。但事实上,嵌套本身并非异步编程的核心痛点。上述代码可以重构为以下形式:
代码如下:
...
function ReadIPCallback(err, ips) {
if (err) {
handlerError(err)
} else {
ips2geos(ips, ips2geosCallback)
}
}
function ips2geosCallback(err, geos) {
if (err) {
handlerError(err)
} else {
geos2weathers(geos, geos2weathersCallback)
}
}
function geos2weathersCallback(err, weathers) {
if (err) {
handlerError(err)
} else {
writeWeather(weathers, writeWeatherCallback)
}
}
function writeWeatherCallback(err) {
if (err) {
handlerError(err)
} else {
console.log('success!')
}
}
readIP('./ip.json', ReadIPCallback)
以上就是 callback.js 文件的完整内容。运行该脚本:
代码如下:
node callback.js运行后将生成 weather.json 文件,内容如下:
代码如下:
[
{
"ip": "180.153.132.38",
"weather": "Clear",
"region": "Shanghai"
},
{
"ip": "91.239.201.98",
"weather": "Clouds"
},
{
"ip": "60.28.215.115",
"weather": "Clear",
"region": "Tianjin"
},
{
"ip": "74.125.235.224",
"weather": "Clouds",
"region": "California"
},
{
"ip": "115.29.230.208",
"weather": "Clear",
"region": "Zhejiang"
}
]
Callback 模式的问题分析
那么,真正的问题究竟在哪里?
当然还是异步处理本身。异步操作本质上需要处理三件事:
1. 异步操作何时结束?需要一种通知机制,Callback 是一种解决方案; 2. 异步操作产生的结果如何传递?Callback 接受一个 data 参数来返回数据; 3. 异步操作出错时如何处理?Callback 接受一个 err 参数来返回错误信息。
但你是否注意到代码中存在大量重复的工作(各种 callback 函数)?上述代码是否存在其他问题?请期待本文的后续章节。
Java和JavaScript名字像但没关系?详解两者及HTML的技术分工
2026-11-03 20:24:10
Node.js 中 CommonJS 与 ES Modules 模块规范的区别及使用
2026-11-03 15:35:13
先学JS还是先学Java?全栈开发视角的学习路径规划
2026-11-03 13:35:26
解决Node.js安装后提示“node不是内部或外部命令”
2026-11-03 08:39:43
生产环境升级 Node.js 的风险评估与 LTS 版本选择
2026-11-02 10:30:14
