个人github博客 https://github.com/zhuanyongxigua/blogjavascript
你们都知道Promise解决了回调地狱的问题。说到回调地狱,很容易想到下面这个容易让人产生误解的图片:java
可回调地狱究竟是什么?它到底哪里有问题?是由于嵌套很差看仍是读起来不方便?node
首先咱们要想一想,嵌套到底哪里有问题?git
举个例子:程序员
function a() { function b() { function c() { function d() {} d(); } c(); } b(); } a();
这也是嵌套,虽然好像不是特别美观,可咱们并不会以为这有什么问题吧?由于咱们常常会写出相似的代码。github
在这个例子中的嵌套的问题仅仅是缩进的问题,而缩进除了会让代码变宽可能会形成读代码的一点不方便以外,并无什么其余的问题。若是仅仅是这样,为何不叫“缩进地狱”或“嵌套地狱”?面试
把回调地狱彻底理解成缩进的问题是常见的对回调地狱的误解。要回到“回调地狱”这个词语上面来,它的重点就在于“回调”,而“回调”在JS中应用最多的场景固然就是异步编程了。ajax
因此,“回调地狱”所说的嵌套实际上是指异步的嵌套。它带来了两个问题:可读性的问题和信任问题。编程
这是一个在网上随便搜索的关于执行顺序的面试题:浏览器
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》的例子:一个程序员开发了一个付款的系统,它良好的运行了很长时间。忽然有一天,一个客户在付款的时候信用卡被连续刷了五次。这名程序员在调查了之后发现,一个第三方的工具库由于某些缘由把付款回调执行了五次。在与第三方团队沟通以后问题获得了解决。
故事讲完了,可问题真的解决了吗?是否还可以充分的信任这个工具库?信任依然要有,可完善必要的检查和错误处理势在必行。当咱们解决了这个问题,因为它的启发,咱们还会联想到其余的问题,好比没有调用回调。
再继续想,你会发现,这样的问题还要好多好多。总结一下可能会出现的问题:
加上了这些检查,强壮以后的代码多是这样的:
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已是原生支持的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只能被决议一次,且决议以后没法改变,因此,即使是屡次回调,也不会影响结果,决议以后的调用都会被忽略。
参考资料: