尝试拯救回调

有几种回调的设计试图解决一些(不是全部!)我们刚才看到的信任问题。这是一种将回调模式从它自己的崩溃中拯救出来的勇敢,但注定失败的努力。

举个例子,为了更平静地处理错误,有些API设计提供了分离的回调(一个用作成功的通知,一个用作错误的通知):

  1. function success(data) {
  2. console.log( data );
  3. }
  4. function failure(err) {
  5. console.error( err );
  6. }
  7. ajax( "http://some.url.1", success, failure );

在这种设计的API中,failure()错误处理器通常是可选的,而且如果不提供的话它会假定你想让错误被吞掉。呃。

注意: ES6的Promises的API使用的就是这种分离回调设计。我们将在下一章中详尽地讨论ES6的Promises。

另一种常见的回调设计模式称为“错误优先风格”(有时称为“Node风格”,因为它几乎在所有的Node.js的API中作为惯例使用),一个回调的第一个参数为一个错误对象保留(如果有的话)。如果成功,这个参数将会是空/falsy(而其他后续的参数将是成功的数据),但如果出现了错误的结果,这第一个参数就会被设置/truthy(而且通常没有其他东西会被传递了):

  1. function response(err,data) {
  2. // 有错?
  3. if (err) {
  4. console.error( err );
  5. }
  6. // 否则,认为成功
  7. else {
  8. console.log( data );
  9. }
  10. }
  11. ajax( "http://some.url.1", response );

这两种方法都有几件事情应当注意。

首先,它们没有像看起来那样真正解决主要的信任问题。在这两个回调中没有关于防止或过滤意外的重复调用的东西。而且,事情现在更糟糕了,因为你可能同时得到成功和失败信号,或者都得不到,你仍然不得不围绕着这两种情况写代码。

还有,不要忘了这样的事实:虽然它们是你可以引用的标准模式,但它们绝对更加繁冗,而且是不太可能复用的模板代码,所以你将会对在你应用程序的每一个回调中敲出它们感到厌倦。

回调从不被调用的信任问题怎么解决?如果这要紧(而且它可能应当要紧!),你可能需要设置一个超时来取消事件。你可以制作一个工具来帮你:

  1. function timeoutify(fn,delay) {
  2. var intv = setTimeout( function(){
  3. intv = null;
  4. fn( new Error( "Timeout!" ) );
  5. }, delay )
  6. ;
  7. return function() {
  8. // 超时还没有发生?
  9. if (intv) {
  10. clearTimeout( intv );
  11. fn.apply( this, [ null ].concat( [].slice.call( arguments ) ) );
  12. }
  13. };
  14. }

这是你如何使用它:

  1. // 使用“错误优先”风格的回调设计
  2. function foo(err,data) {
  3. if (err) {
  4. console.error( err );
  5. }
  6. else {
  7. console.log( data );
  8. }
  9. }
  10. ajax( "http://some.url.1", timeoutify( foo, 500 ) );

另一个信任问题是被调用的“过早”。在应用程序规范上讲,这可能涉及在某些重要的任务完成之前被调用。但更一般地,在那些即可以 现在(同步地),也可以在 稍后(异步地)调用你提供的回调的工具中这个问题更明显。

这种围绕着同步或异步行为的不确定性,几乎总是导致非常难追踪的Bug。在某些圈子中,一个名叫Zalgo的可以导致人精神错乱的虚构怪物被用来描述这种同步/异步的噩梦。经常能听到人们喊“别放出Zalgo!”,而且它引出了一个非常响亮的建议:总是异步地调用回调,即便它是“立即”在事件轮询的下一个迭代中,这样所有的回调都是可预见的异步。

注意: 更多关于Zalgo的信息,参见Oren Golan的“Don’t Release Zalgo!(不要释放Zalgo!)”(https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md)和Isaac Z. Schlueter的“Designing APIs for Asynchrony(异步API设计)”(http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony)。

考虑下面的代码:

  1. function result(data) {
  2. console.log( a );
  3. }
  4. var a = 0;
  5. ajax( "..pre-cached-url..", result );
  6. a++;

这段代码是打印0(同步回调调用)还是打印1(异步回调调用)?这……要看情况。

你可以看到Zalgo的不可预见性能有多快地威胁你的JS程序。所以听起来傻呼呼的“别放出Zalgo”实际上是一个不可思议地常见且实在的建议——总是保持异步。

如果你不知道当前的API是否会总是异步地执行呢?你可以制造一个像asyncify(..)这样的工具:

  1. function asyncify(fn) {
  2. var orig_fn = fn,
  3. intv = setTimeout( function(){
  4. intv = null;
  5. if (fn) fn();
  6. }, 0 )
  7. ;
  8. fn = null;
  9. return function() {
  10. // 触发太快,在`intv`计时器触发来
  11. // 表示异步回合已经过去之前?
  12. if (intv) {
  13. fn = orig_fn.bind.apply(
  14. orig_fn,
  15. // 将包装函数的`this`加入`bind(..)`调用的
  16. // 参数,同时currying其他所有的传入参数
  17. [this].concat( [].slice.call( arguments ) )
  18. );
  19. }
  20. // 已经是异步
  21. else {
  22. // 调用原版的函数
  23. orig_fn.apply( this, arguments );
  24. }
  25. };
  26. }

你像这样使用asyncify(..):

  1. function result(data) {
  2. console.log( a );
  3. }
  4. var a = 0;
  5. ajax( "..pre-cached-url..", asyncify( result ) );
  6. a++;

不管Ajax请求是由于存在于缓存中而解析为立即调用回调,还是它必须走过网线去取得数据而异步地稍后完成,这段代码总是输出1而不是0——result(..)总是被异步地调用,这意味着a++有机会在result(..)之前运行。

噢耶,又一个信任问题被“解决了”!但它很低效,而且又有更多臃肿的模板代码让你的项目变得沉重。

这只是关于回调一遍又一遍地发生的故事。它们几乎可以做任何你想做的事,但你不得不努力工作来达到目的,而且大多数时候这种努力比你应当在推理这样的代码上所付出的多得多。

你可能发现自己希望有一些内建的API或语言机制来解决这些问题。终于ES6带着一个伟大的答案到来了,所以继续读下去!