返回介绍

第一部分 类型和语法

第二部分 异步和性能

2.4 省点回调

发布于 2023-05-24 16:38:21 字数 4058 浏览 0 评论 0 收藏 0

回调设计存在几个变体,意在解决前面讨论的一些信任问题(不是全部!)。这种试图从回调模式内部挽救它的意图是勇敢的,但却注定要失败。

举例来说,为了更优雅地处理错误,有些 API 设计提供了分离回调 (一个用于成功通知,一个用于出错通知):

function success(data) {
  console.log( data );
}

function failure(err) {
  console.error( err );
}

ajax( "http://some.url.1", success, failure );

在这种设计下,API 的出错处理函数 failure() 常常是可选的,如果没有提供的话,就是假定这个错误可以吞掉。

ES6 Promise API 使用的就是这种分离回调设计。第 3 章会介绍 ES6 Promise 的更多细节。

还有一种常见的回调模式叫作“error-first 风格”(有时候也称为“Node 风格”,因为几乎所有 Node.js API 都采用这种风格),其中回调的第一个参数保留用作错误对象(如果有的话)。如果成功的话,这个参数就会被清空 / 置假(后续的参数就是成功数据)。不过,如果产生了错误结果,那么第一个参数就会被置起 / 置真(通常就不会再传递其他结果):

function response(err,data) {
  // 出错?
  if (err) {
    console.error( err );
  }
  // 否则认为成功
  else {
    console.log( data );
  }
}

ajax( "http://some.url.1", response );

在这两种情况下,都应该注意到以下几点。

首先,这并没有像表面看上去那样真正解决主要的信任问题。这并没有涉及阻止或过滤不想要的重复调用回调的问题。现在事情更糟了,因为现在你可能同时得到成功或者失败的结果,或者都没有,并且你还是不得不编码处理所有这些情况。

另外,不要忽略这个事实:尽管这是一种你可以采用的标准模式,但是它肯定更加冗长和模式化,可复用性不高,所以你还得不厌其烦地给应用中的每个回调添加这样的代码。

那么完全不调用这个信任问题又会怎样呢?如果这是个问题的话(可能应该是个问题!),你可能需要设置一个超时来取消事件。可以构造一个工具(这里展示的只是一个“验证概念”版本)来帮助实现这一点:

function timeoutify(fn,delay) {
  var intv = setTimeout( function(){
      intv = null;
      fn( new Error( "Timeout!" ) );
    }, delay )
  ;

  return function() {
    // 还没有超时?
    if (intv) {
      clearTimeout( intv );
      fn.apply( this, arguments );
    }
  };
}

以下是使用方式:

// 使用"error-first 风格" 回调设计
function foo(err,data) {
  if (err) {
    console.error( err );
  }
  else {
    console.log( data );
  }
}

ajax( "http://some.url.1", timeoutify( foo, 500 ) );

还有一个信任问题是调用过早。在特定应用的术语中,这可能实际上是指在某个关键任务完成之前调用回调。但是更通用地来说,对于既可能在现在 (同步)也可能在将来 (异步)调用你的回调的工具来说,这个问题是明显的。

这种由同步或异步行为引起的不确定性几乎总会带来极大的 bug 追踪难度。在某些圈子里,人们用虚构的十分疯狂的恶魔 Zalgo 来描述这种同步 / 异步噩梦。常常会有“不要放出 Zalgo”这样的呼喊,而这也引出了一条非常有效的建议:永远异步调用回调,即使就在事件循环的下一轮,这样,所有回调就都是可预测的异步调用了。

关于 Zalgo 的更多信息,可以参考 Oren Golan 的“Don't Release Zalgo!”(https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md )以及 Issac Z. Schlueter 的 “Designing APIs for Asynchrony”(http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony )。

考虑:

function result(data) {
  console.log( a );
}

var a = 0;

ajax( "..pre-cached-url..", result );
a++;

这段代码会打印出 0 (同步回调调用)还是 1 (异步回调调用)呢?这要视情况而定。

你可以看出 Zalgo 的不确定性给 JavaScript 程序带来的威胁。所以听上去有点傻的“不要放出 Zalgo”实际上十分常用,并且也是有用的建议。永远要异步。

如果你不确定关注的 API 会不会永远异步执行怎么办呢?可以创建一个类似于这个“验证概念”版本的 asyncify(..) 工具:

function asyncify(fn) {
  var orig_fn = fn,
    intv = setTimeout( function(){
      intv = null;
      if (fn) fn();
    }, 0 )
  ;

  fn = null;

  return function() {
    // 触发太快,在定时器intv触发指示异步转换发生之前?
    if (intv) {
      fn = orig_fn.bind.apply(
        orig_fn,
         // 把封装器的this添加到bind(..)调用的参数中,
         // 以及克里化(currying)所有传入参数
         [this].concat( [].slice.call( arguments ) )
       );
     }
     // 已经是异步
     else {
       // 调用原来的函数
       orig_fn.apply( this, arguments );
     }
   };
 }

可以像这样使用 asyncify(..) :

function result(data) {
  console.log( a );
}

var a = 0;

ajax( "..pre-cached-url..", asyncify( result ) );
a++;

不管这个 Ajax 请求已经在缓存中并试图对回调立即调用,还是要从网络上取得,进而在将来异步完成,这段代码总是会输出 1 ,而不是 0 ——result(..) 只能异步调用,这意味着 a++ 有机会在 result(..) 之前运行。

好啊,又“解决”了一个信任问题!但这是低效的,而且也会带来膨胀的重复代码,使你的项目变得笨重。

这就是回调的故事,讲了一遍又一遍。它们可以实现所有你想要的功能,但是你需要努力才行。这些努力通常比你追踪这样的代码能够或者应该付出的要多得多。

可能现在你希望有内建的 API 或其他语言机制来解决这些问题。最终,ES6 带着一些极好的答案登场了,所以,继续读下去吧!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文