Node.js 异步编程之 Callback介绍(一)


Posted in Javascript onMarch 30, 2015

Node.js 基于 JavaScript 引擎 v8,是单线程的。Node.js 采用了与通常 Web 上的 JavaScript 异步编程的方式来处理会造成阻塞的I/O操作。在 Node.js 中读取文件、访问数据库、网络请求等等都有可能是异步的。对于 Node.js 新人或者从其他语言背景迁移到 Node.js 上的开发者来说,异步编程是比较痛苦的一部分。本章将由浅入深为大家讲解 Node.js 异步编程的方方面面。从最基础的 callback 到 thunk、Promise、co 直到 ES7 计划的 async/await。

首先我们先从一个具体的异步编程的例子说起。

获取多个 ip 所在地的天气信息

在 ip.json 这个文件中,有一个数组我们存放了若干个 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 地址获取 ip 所在地的地理位置;
3.根据地理位置查询当地的天气;
4.将结果写入到 weather.json 文件中。

这些步骤都是异步的(读写文件可以同步,但作为示例,都用异步)。

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 数据来获取 weather:

...

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 已经并行地读取 weather 数据:
...

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 异步的问题,说真的,嵌套不是 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

将会生成 weater.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"

  }

]

那正真的问题是什么?

当然是异步的问题啦,异步本质上要处理三个事情:

1.异步操作什么时候结束,需要通知回来,Callback 是一种方案;
2.异步产生的结果需要传递回来,Callback 接受一个 data 参数,把数据传回来;
3.异步如果出错了怎么办?Callback 接受 一个 err 参数,把错误传回来。

但有没有发现好多重复的工作(各种 callback)?上面的这些代码有什么问题么?请大家期待本文的续篇。

Javascript 相关文章推荐
Mootools 1.2 手风琴(Accordion)教程
Sep 15 Javascript
jquery cookie实现的简单换肤功能适合小网站
Aug 25 Javascript
jQuery插件 selectToSelect使用方法
Oct 02 Javascript
在javascript中如何得到中英文混合字符串的长度
Jan 17 Javascript
js取float型小数点后两位数的方法
Jan 18 Javascript
javascript中拼接HTML字符串的最快、最好的方法
Jun 07 Javascript
JS判断变量是否为空判断是否null
Jul 25 Javascript
jquery判断复选框是否选中进行答题提示特效
Dec 10 Javascript
AngularJS控制器controller正确的通信的方法
Jan 25 Javascript
JS实现显示带倒影的图片横排居中放大展示特效实例【测试可用】
Aug 23 Javascript
React如何利用相对于根目录进行引用组件详解
Oct 09 Javascript
Nuxt页面级缓存的实现
Mar 09 Javascript
js动态修改表格行colspan列跨度的方法
Mar 30 #Javascript
jQuery使用hide方法隐藏页面上指定元素的方法
Mar 30 #Javascript
jquery使用hide方法隐藏指定id的元素
Mar 30 #Javascript
jQuery使用hide方法隐藏指定元素class样式用法实例
Mar 30 #Javascript
jQuery使用hide方法隐藏元素自身用法实例
Mar 30 #Javascript
javascript实现控制浏览器全屏
Mar 30 #Javascript
原生JavaScript编写俄罗斯方块
Mar 30 #Javascript
You might like
PHP+FFMPEG实现将视频自动转码成H264标准Mp4文件
2014/09/24 PHP
ThinkPHP添加更新标签的方法
2014/12/05 PHP
php实现计算百度地图坐标之间距离的方法
2016/05/05 PHP
laravel如何开启跨域功能示例详解
2017/08/31 PHP
用htc组件制作windows选项卡
2007/01/13 Javascript
js获取html文件的思路及示例
2013/09/17 Javascript
获得Javascript对象属性个数的示例代码
2013/11/21 Javascript
带左右箭头图片轮播的JS代码
2013/12/18 Javascript
Vue2.0权限树组件实现代码
2017/08/29 Javascript
vue中实现methods一个方法调用另外一个方法
2018/02/08 Javascript
nodeJs实现基于连接池连接mysql的方法示例
2018/02/10 NodeJs
Vue 项目分环境打包的方法示例
2018/08/03 Javascript
JS实现的简单分页功能示例
2018/08/23 Javascript
JavaScript定时器常见用法实例分析
2019/11/15 Javascript
JavaScript数值类型知识汇总
2019/11/17 Javascript
Vue+Node实现的商城用户管理功能示例
2019/12/23 Javascript
JS数组push、unshift、pop、shift方法的实现与使用方法示例
2020/04/29 Javascript
openlayers4.6.5实现距离量测和面积量测
2020/09/25 Javascript
玩转python爬虫之正则表达式
2016/02/17 Python
Python爬虫利用cookie实现模拟登陆实例详解
2017/01/12 Python
Python绘制的二项分布概率图示例
2018/08/22 Python
对python3 Serial 串口助手的接收读取数据方法详解
2019/06/12 Python
Django如何批量创建Model
2020/09/01 Python
Pycharm中使用git进行合作开发的教程详解
2020/11/17 Python
纯CSS3实现8组超炫酷鼠标滑过图片动画
2016/03/16 HTML / CSS
英国女性时尚鞋类的潮流制造者:Koi Footwear
2018/10/19 全球购物
促销活动方案模板
2014/02/24 职场文书
毕业典礼演讲稿
2014/05/13 职场文书
董事长秘书工作职责
2014/06/10 职场文书
2014年重阳节老干部座谈会上的讲话稿
2014/09/25 职场文书
离婚承诺书格式范文
2015/05/04 职场文书
2015年度校学生会工作总结报告
2015/05/23 职场文书
公务员爱岗敬业心得体会
2016/01/25 职场文书
go开发alertmanger实现钉钉报警
2021/07/16 Golang
Spring Bean是如何初始化的详解
2022/03/22 Java/Android
使用CSS定位HTML元素的实现方法
2022/07/07 HTML / CSS