Node.js 异步编程入门:Callback 模式实战解析

作者:慢半拍

时间:2026-10-29

浏览:637

本文深入解析 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 函数)?上述代码是否存在其他问题?请期待本文的后续章节。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章