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 相关文章推荐
fancybox modal的完美解决(右上的X)
Oct 30 Javascript
JQuery的自定义事件代码,触发,绑定简单实例
Aug 01 Javascript
jQuery实现点击文本框弹出热门标签的提示效果
Nov 17 Javascript
jQuery 动态云标签插件
Nov 11 Javascript
jQuery获得子元素个数的方法
Apr 14 Javascript
JavaScript中继承用法实例分析
May 16 Javascript
浅谈Vue.js
Mar 02 Javascript
jQuery EasyUI的TreeGrid查询功能实现方法
Aug 08 jQuery
在一个页面实现两个zTree联动的方法
Dec 20 Javascript
vue图片上传本地预览组件使用详解
Feb 20 Javascript
详解使用Nuxt.js快速搭建服务端渲染(SSR)应用
Mar 13 Javascript
通过实例解析json与jsonp原理及使用方法
Sep 27 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编程函数安全篇
2013/01/08 PHP
PHP实现的堆排序算法详解
2017/08/17 PHP
jquery聚焦文本框与扩展文本框聚焦方法
2012/10/12 Javascript
jQuery+css实现图片滚动效果(附源码)
2013/03/18 Javascript
js统计录入文本框中字符的个数并加以限制不超过多少
2014/05/23 Javascript
JS输入用户名自动显示邮箱后缀列表的方法
2015/01/27 Javascript
jQuery搜索子元素的方法
2015/02/10 Javascript
jQuery+Ajax实现无刷新分页
2015/10/30 Javascript
基于jQuery的网页影音播放器jPlayer的基本使用教程
2016/03/08 Javascript
jQuery插件实现文件上传功能(支持拖拽)
2020/08/27 Javascript
jQuery的事件预绑定
2016/12/05 Javascript
MUI实现上拉加载和下拉刷新效果
2017/06/30 Javascript
微信小程序开发之animation循环动画实现的让云朵飘效果
2017/07/14 Javascript
JavaScript 完成注册页面表单校验的实例
2017/08/19 Javascript
jQuery UI 实例讲解 - 日期选择器(Datepicker)
2017/09/18 jQuery
js实现关闭网页出现是否离开提示
2017/12/07 Javascript
Vue递归实现树形菜单方法实例
2018/11/06 Javascript
JS数组扁平化(flat)方法总结详解
2019/06/24 Javascript
vue自动化路由的实现代码
2019/09/30 Javascript
js前端如何写一个精确的倒计时代码
2019/10/25 Javascript
vue项目中定义全局变量、函数的几种方法
2019/11/08 Javascript
js实现上传按钮并显示缩略图小轮子
2020/05/04 Javascript
[15:58]DOTA2国际邀请赛采访专栏:Tongfu.Sansheng&KingJ,DK.rOtk
2013/08/08 DOTA
[03:42]2016国际邀请赛中国区预选赛首日现场玩家采访
2016/06/26 DOTA
[57:29]Alliance vs KG 2019国际邀请赛小组赛 BO2 第二场 8.16
2019/08/17 DOTA
python3获取当前文件的上一级目录实例
2018/04/26 Python
pygame游戏之旅 添加游戏介绍
2018/11/20 Python
详解python中递归函数
2019/04/16 Python
Python pandas DataFrame操作的实现代码
2019/06/21 Python
马来西亚最好的婴儿商店:Motherhood
2017/09/14 全球购物
三陽商会官方网站:Sanyo iStore
2019/05/15 全球购物
军用级手机壳,专为冒险而建:Zizo Wireless
2019/08/07 全球购物
高中毕业自我鉴定
2013/12/22 职场文书
医院学雷锋活动策划方案
2014/02/15 职场文书
2015年会计个人工作总结
2015/04/02 职场文书
党风廉政承诺书2016
2016/03/25 职场文书