2.4 省点回调
回调设计存在几个变体,意在解决前面讨论的一些信任问题(不是全部!)。这种试图从回调模式内部挽救它的意图是勇敢的,但却注定要失败。
举例来说,为了更优雅地处理错误,有些 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论