目前为止,异步的实现靠回调,但它并非没有缺点。本章探讨回调,并解释为什么更高级的异步模型是必需的。
只有理解了promise出现的原因,才能更好地使用promise。
1.continuation
回调函数包裹或者说封装了程序的延续。
2.顺序的大脑
我们大多数人对任务的理解是单线程的、线性的,在实际操作中,我们在一段时间里同时进行多件事,是因为我们在不同的任务之间切换上下文。
所以,虽然在执行层面我们也像JavaScript引擎一样有“异步”,但是我们习惯以顺序的、同步的方式理解任务计划。
因此,用回调来实现异步,与我们大脑习惯的思考方式有冲突,这使得回调代码难以阅读。
嵌套的回调不易确定执行顺序,首先它阅读起来难受,第二,就算读懂了,如果嵌套的方法里有些不是异步的,而是同步的,或者会视情况而定同步还是异步,就容易搞不清执行顺序,出了bug不好找出来。
那么一环扣一环,一条龙式地写异步回调呢?它会导致硬编码,硬编码不适合需要区分多种情况的流程,只适合基本上一条道走到底的那种流程。
3.信任问题
回调需要把代码丢给第三方来调用,但第三方不一定会按你的构想来调用,对方可能会出岔子,回调调早了、调迟了、调多了、调少了。
回调最大的问题是控制反转,它会导致信任链的完全断裂。
4.省点回调
为了更优雅地处理错误,有些 API 设计提供了分离回调(一个用于成功通知, 一个用于出错通知)。
ES6 Promise API 使用的就是这种分离回调设计。
常见的“error-first风格”,也叫Node风格(因为Node.js API基本都用这种风格),它向回调传入的第一个参数就是错误对象,如果成功,第一个参数就为空。
不过这样仍然没有解决信任问题,第三方可能同时调成功回调和失败回调,或者都不调。反正自己还是要写许多防范的判断。
还有一点比较可怕的是,第三方万一不是个异步的,而是个同步的,那么回调函数会被同步地,而不是异步地执行。
为了防范这个问题,可以在回调方法外面套一层判断,用定时器setTimeout(..0)是否已结束,来判断回调被同步调用还是异步调用,使回调函数一定被异步执行。
“防范回调被同步调用”解决了,但也带来膨胀的重复代码,项目变得笨重。
回调可以实现所有你想要的功能,但是需要付出太多精力,这些精力通常比你追踪这样的代码能够或者应该付出的要多得多。
总结:
大脑的思维方式是宏观上(在较大的任务层面上)上线性、单线程,尽管在具体执行上我们经常在不同任务之间切换。用回调来表达异步,是非线性的,不利于我们对代码逻辑的阅读理解。
另外,回调把控制权交给了第三方,我们需要一个通用的方案来解决这些信任问题。不管我们创建多少回调,这一方案都应可以复用,且没有重复代码的开销。