
// A
setTimeout(function() {
// C
},1000);
// B
有些人会描述,先执行A,设置延时1000毫秒,然后执行B,1000毫秒到时后执行C。这样解释是不足的(不够精确),这也是回调作为异步表达和管理方式的缺陷的关键所在。如果存在多个回调函数,那么就会难以理解、追踪、调试和维护。
二、顺序的大脑
我们大脑的工作方式优点类似于事件循环队列。大脑在特定的时候,只能思考一件事情。我们好像并行执行多个任务,但实际上可能是快速的上下文切换。也就是在多个任务之间快速地来回切换。我们切换得非常快,对于外界来说,我们就像在并行地执行任务。
1. 执行与计划
我们大脑先安排顺序,然后按照顺序(A,然后B,然后C)执行,如果有某种形式的拥塞,会保证B等待A完成,C等待A完成。开发者编写代码的时候是在计划一系列动作的发生。问题是,代码(通过回调)表达异步的方式并不能很好地映射到同步的大脑计划行为。我们思考方式是一步一步的,但是从同步切换到异步后,回调却不是按照一步一步的方式来表达。这就是为什么精确编写和追踪使用回调的异步JavaScript困难的原因。
2. 嵌套回到和链式回调
多个函数嵌套在一起,每个函数都代表异步序列(多个有顺序的异步任务)中的一个步骤,这种代码常常被称为回调地狱。
顺序匹配
实际上,回调地狱与嵌套和缩进几乎没有什么关系。这只是其中的一个问题,它引起的问题要比这个严重得多。// 伪代码,function()代表回调函数
doA(function() {
doB();
doC(function() {
doD();
})
doE();
});
doF();
尽管有经验的你能够正确描述出实际的运行顺序,但这需要费一番脑筋才能想清楚。实际顺序:doA() => doF() => doB() => doC() => doE() => doD()我们不得不上下来回查看哪个函数先被调用。想象一下如果异步任务更多,那么将更加复杂。但是,这是假定异步的情况下。如果doA()和doD()实际并不是异步,或者在某些情况下并不是异步的,那么这个顺序就更加困难了。
脆弱性
这样硬编码还会使代码更脆弱,因为它没有考虑可能导致步骤执行过程中的异常情况。
一旦你指定了所有可能事件和路径,代码就会变得非常复杂。
比如,如果步骤2失败,就永远不会到达步骤3,不管是重试还是跳转到其他错误流程。这个问题也可以解决,但是代码通常是重复的。(成功和失败的程序中的代码重复了)
这才是回调地狱的真正问题所在。
三、信任问题回调最大的问题是控制反转,它会导致信任链完全断裂。
我们把自己程序一部分的执行控制交给某个第三方,称为控制反转。
我们使用第三方库的API,将回调函数传入的时候,可能会导致一些问题。
比如:在这个第三方库里面,因为某些错误将这个回调执行了多次,你可以只要它执行一次,但这不是你可以控制的。
当然,不只是次数问题,还有
调用回调过早调用回调过晚(或没有调用)调用次数过多或过少没有成功地将参数传入到回调中吞掉可能出现的报错和异常这不只是针对外部代码,对于我们自己控制下的代码,也可能会有问题。
四、尝试挽救回调回调设计存在几个变体,意在解决前面讨论的一些信任问题。
1. 处理错误 分离回调设计:一个用于成功通知,一个用于出错通知。比如ES6的Promise API。error-first风格:回调中第一个参数预留作为错误对象,如果成功,这个参数会置假。 2. 调用过早 对于既可能在现在(同步)也可能在将来(异步)调用你的回调的工具来说,会有明显的问题。这种由同步或异步行为引起的不确定性总会带来极大的bug追踪难度。永远异步调用回调,这样所有回调都是可以预测的异步调用了。欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)