寸志

Node.js 异步编程之 Callback的问题

在上一篇中,我们使用 Callback 的方式来实现了我们需求,将一个 IP 列表转换成了具体的城市和天气。可以看出回调嵌套并不是 Callback 作为异步处理方案的真真问题。那正真的问题是什么呢?

可靠性

上篇文章发出来,就被 @朴灵 吐槽,我还浑然不知。看上一篇文章中的这段代码:

try {
  data = JSON.parse(data)
  callback(null, data)
} catch (error) {
  callback(error)
}

事实上,在这段代码中,callback 有可能被调用两次。

这个问题苏千在12年的沪JS大会上已经讲过了,我是现场观众,但却忘记了……

如果JSON.parse 成功,但是 callback 在运行的时候报异常的话,就会触发 catch 块,callback 就会再被调用一次。这个问题不难理解,但是非常隐蔽。下面是可行的一种 fix 方案:

var hasError = false
try {
  data = JSON.parse(data)
} catch (e) {
  err = e
  hasError = true
}
if (hasError) {
  callback(err)
} else {
  callback(null, data)
}

其实不仅仅是上面这段代码,看下面这一段:

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)
}

这段代码来自 ips2geos 函数,这个函数就是实现并行地异步读取多个 IP 地址的 geo 数据,读取成功后组装成数组返回给 callback;但如果某个异步读取过程出错了,就直接调用 callback 将错误信息返回。但在这段代码中,callback 很可能被调用多次,这种情况出现在有多个异步 IP 转 geo 出错的时候。一种还算凑活的修正:

var returned = false
for (var i = 0; i < ips.length; i++) {
  ip = ips[i];
  (function(ip) {
    ip2geo(ip, function(err, geo) {
      if (returned) {
        return
      }
      if (err) {
        callback(err)
        returned = true
      } else {
        geo.ip = ip
        geos.push(geo)
        remain--
      }
      if (remain == 0) {
        callback(null, geos)
      }
    })
  })(ip)
}

这就是 callback 的可靠性问题。每个以 callback 作为异步回调逻辑都可能产生问题。我们自己写的代码,或者第三方类库都有可能导致 callback 被重复调用。以 callback 提供的异步 API 是无法保证回调次数的,这就产生了信任问题。如果有大量的异步嵌套,只要出错,就是一场灾难。

很难处理串/并行异步操作

串并行的异步操作大大提高了程序的复杂度,而直白的 callback 拿这个问题没有太大的办法。

作为写程序的开发者,同步逻辑更容易理解,更直观。可以像下面这样:

串行逻辑:

var ips = readIP('./ip.json')
var geos = ips2geos(ips)
var weathers = geos2weathers(geos)
...

并行逻辑:

function ips2geos(ips) {
  var geos = []
  var ip
  for (var i = 0; i < ips.length; i++) {
    ip = ips[i]
    geos.push(ip2geo(ip))
  }
  return geos
}

很简单不是。

像上面这样的代码,fibjs 可以做到,fibjs 把异步串/并行做到了自己的内部实现中。

异步打破了程序运行的正常顺序,而 callback 的表现力非常不足,稍微复杂的处理逻辑代码写起来就一团糟。见第一节的 ips2geos 等函数。

总结

在本文中我们指出了 callback 作为异步处理的两个比较严重的问题,异步本身并不是坏事,只是 callback 的方案缺乏可靠性,表现力不足。在下一篇文章中我就进入正题,开始给大家介绍 thunk 以及 thunks 类库。后者是 @严清 开发的一个类库,灵感来自于 co,意在提升异步编程的体验。敬请期待。