我的 github https://github.com/zhuanyongxigua/blog
大家都知道 Promise 解决了回调地狱的问题. 说到回调地狱, 很容易想到下面这个容易让人产生误解的图片:
可回调地狱到底是什么? 它到底哪里有问题? 是因为嵌套不好看还是读起来不方便?
首先我们要想想, 嵌套到底哪里有问题?
举个例子:
- function a() {
- function b() {
- function c() {
- function d() {}
- d();
- }
- c();
- }
- b();
- }
- a();
复制代码
这也是嵌套, 虽然好像不是特别美观, 可我们并不会觉得这有什么问题吧? 因为我们经常会写出类似的代码.
在这个例子中的嵌套的问题仅仅是缩进的问题, 而缩进除了会让代码变宽可能会造成读代码的一点不方便之外, 并没有什么其他的问题. 如果仅仅是这样, 为什么不叫 "缩进地狱" 或 "嵌套地狱"?
把回调地狱完全理解成缩进的问题是常见的对回调地狱的误解. 要回到 "回调地狱" 这个词语上面来, 它的重点就在于 "回调", 而 "回调" 在 JS 中应用最多的场景当然就是异步编程了.
所以,"回调地狱" 所说的嵌套其实是指异步的嵌套. 它带来了两个问题: 可读性的问题和信任问题.
可读性的问题
这是一个在网上随便搜索的关于执行顺序的面试题:
- for (var i = 0; i <5; i++) {
- setTimeout(function() {
- console.log(new Date, i);
- }, 1000);
- }
- console.log(new Date, i);
复制代码
答案是什么大家自己想吧, 这不是重点. 重点是, 你要想一会儿吧?
一个整洁的回调:
- listen( "click", function handler( evt){
- setTimeout( function request(){
- ajax( "http:// some. url. 1", function response( text){
- if (text == "hello") {
- handler();
- } else if (text == "world") {
- request();
- }
- });
- }, 500);
- });
复制代码
如果异步的嵌套都是这样干净整洁, 那 "回调地狱" 给程序猿带来的伤害马上就会减少很多.
可我们实际在写业务逻辑的时候, 真实的情况应该是这样的:
- listen( "click", function handler(evt){
- doSomething1();
- doSomething2();
- doSomething3();
- doSomething4();
- setTimeout( function request(){
- doSomething8();
- doSomething9();
- doSomething10();
- ajax( "http:// some. url. 1", function response( text){
- if (text == "hello") {
- handler();
- } else if (text == "world") {
- request();
- }
- });
- doSomething11();
- doSomething12();
- doSomething13();
- }, 500);
- doSomething5();
- doSomething6();
- doSomething7();
- });
复制代码
这些 "doSomething" 有些是异步的, 有些是同步. 这样的代码读起来会非常的吃力, 因为你要不停的思考他们的执行顺序, 并且还要记在脑袋里面. 这就是异步的嵌套带来的可读性的问题, 它是由异步的运行机制引起的.
信任问题
这里主要用异步请求讨论. 我们在做 AJAX 请求的时候, 一般都会使用一些第三方的工具库(即便是自己封装的, 也可以在一定程度上理解成第三方的), 这就会带来一个问题: 这些工具库是否百分百的可靠?
一个来自YDKJS https://github.com/getify/You-Dont-Know-JS/blob/master/async & performance/README.md#you-dont-know-js-async--performance 的例子: 一个程序员开发了一个付款的系统, 它良好的运行了很长时间. 突然有一天, 一个客户在付款的时候信用卡被连续刷了五次. 这名程序员在调查了以后发现, 一个第三方的工具库因为某些原因把付款回调执行了五次. 在与第三方团队沟通之后问题得到了解决.
故事讲完了, 可问题真的解决了吗? 是否还能够充分的信任这个工具库? 信任依然要有, 可完善必要的检查和错误处理势在必行. 当我们解决了这个问题, 由于它的启发, 我们还会联想到其他的问题, 比如没有调用回调.
再继续想, 你会发现, 这样的问题还要好多好多. 总结一下可能会出现的问题:
回调过早(一般是异步被同步调用);
回调过晚或没有回调;
回调次数过多;
等等
加上了这些检查, 强壮之后的代码可能是这样的:
- listen( "click", function handler( evt){
- check1();
- doSomething1();
- setTimeout( function request(){
- check2();
- doSomething3();
- ajax( "http:// some. url. 1", function response( text){
- if (text == "hello") {
- handler();
- } else if (text == "world") {
- request();
- }
- });
- doSomething4();
- }, 500);
- doSomething2();
- });
复制代码
我们都清楚的知道, 实际的 check 要比这里看起来的复杂的多, 而且很多很难复用. 这不但使代码变得臃肿不堪, 还进一步加剧了可读性的问题.
虽然这些错误出现的概率不大, 但我们依然必须要处理.
这就是异步嵌套带来的信任问题, 它的问题的根源在于控制反转. 控制反转在面向对象中的应用是依赖注入, 实现了模块间的解耦. 而在回调中, 它就显得没有那么善良了, 控制权被交给了第三方, 由第三方决定什么时候调用回调以及如何调用回调.
一些解决信任问题的尝试
加一个处理错误的回调
- function success(data) {
- console. log(data);
- }
- function failure(err) {
- console. error( err );
- }
- ajax( "http:// some. url. 1", success, failure );
复制代码
nodejs 的 error-first
- function response(err, data) {
- if (err) {
- console. error( err );
- }
- else {
- console. log( data );
- }
- }
- ajax( "http:// some. url. 1", response );
复制代码
这两种方式解决了一些问题, 减少了一些工作量, 但是依然没有彻底解决问题. 首先它们的可复用性依然不强, 其次, 如回调被多次调用的问题依然无法解决.
Promise 如何解决这两个问题
Promise 已经是原生支持的 API 了, 它已经被加到了 JS 的规范里面, 在各大浏览器中的运行机制是相同的. 这样就保证了它的可靠.
如何解决可读性的问题
这一点不用多说, 用过 Promise 的人很容易明白. Promise 的应用相当于给了你一张可以把解题思路清晰记录下来的草稿纸, 你不在需要用脑子去记忆执行顺序.
如何解决信任问题
Promise 并没有取消控制反转, 而是把反转出去的控制再反转一次, 也就是反转了控制反转.
这种机制有点像事件的触发. 它与普通的回调的方式的区别在于, 普通的方式, 回调成功之后的操作直接写在了回调函数里面, 而这些操作的调用由第三方控制. 在 Promise 的方式中, 回调只负责成功之后的通知, 而回调成功之后的操作放在了 then 的回调里面, 由 Promise 精确控制.
Promise 有这些特征: 只能决议一次, 决议值只能有一个, 决议之后无法改变. 任何 then 中的回调也只会被调用一次. Promise 的特征保证了 Promise 可以解决信任问题.
对于回调过早的问题, 由于 Promise 只能是异步的, 所以不会出现异步的同步调用. 即便是在决议之前的错误, 也是异步的, 并不是会产生同步 (调用过早) 的困扰.
- var a = new Promise((resolve, reject) => {
- var b = 1 + c; // ReferenceError: c is not defined, 错误会在下面的 a 打印出来之后报出.
- resolve(true);
- })
- console.log(1, a);
- a.then(res => {
- console.log(2, res);
- })
- .catch(err => {
- console.log(err);
- })
复制代码
对于回调过晚或没有调用的问题, Promise 本身不会回调过晚, 只要决议了, 它就会按照规定运行. 至于服务器或者网络的问题, 并不是 Promise 能解决的, 一般这种情况会使用 Promise 的竞态 APIPromise.race 加一个超时的时间:
- function timeoutPromise(delay) {
- return new Promise(function(resolve, reject) {
- setTimeout(function() {
- reject("Timeout!");
- }, delay);
- });
- }
- Promise.race([doSomething(), timeoutPromise(3000)])
- .then(...)
- .catch(...);
复制代码
对于回调次数太少或太多的问题, 由于 Promise 只能被决议一次, 且决议之后无法改变, 所以, 即便是多次回调, 也不会影响结果, 决议之后的调用都会被忽略.
参考资料:
You Don't Know JS: Async & Performance https://github.com/getify/You-Dont-Know-JS/blob/master/async & performance/README.md#you-dont-know-js-async--performance
来源: https://juejin.im/post/5b45bea65188251b1c3ce1ec