优秀的编程知识分享平台

网站首页 > 技术文章 正文

高级JavaScript程序员必须掌握的一个问题:如何解决回调的信任问题

nanyue 2024-08-05 20:02:38 技术文章 8 ℃

回调设计存在几个变体,意在解决一些信任问题(不是全部!)。这种试图从回调模式内部挽救它的意图很好,但却注定不是最好的解决办法。举例来说,为了更好地处理错误,有些 API 设计提供了分离回调(一个用于成功通知,一个用于出错通知):

function success(data) {

console.log( data );

}

function failure(err) {

console.error( err );

}

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

在这种设计下, API 的出错处理函数 failure() 常常是可选的,如果没有提供的话,就是假定这个错误可以吞掉。还有一种常见的回调模式叫作“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”这样的呼喊,而这也引出了一条非常有效的建议:永远异步调用回调,即使就在事件循环的下一轮,这样,所有回调就都是可预测的异步调用了。

考虑:

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 的文章,能帮你很好的解决这一问题!

Tags:

最近发表
标签列表