本文转载于:猿2048网站⇨https://www.mk2048.com/blog/blog.php?id=h12kij2jaa
我的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》的例子:一个程序员开发了一个付款的系统,它良好的运行了很长时间。突然有一天,一个客户在付款的时候信用卡被连续刷了五次。这名程序员在调查了以后发现,一个第三方的工具库因为某些原因把付款回调执行了五次。在与第三方团队沟通之后问题得到了解决。
故事讲完了,可问题真的解决了吗?是否还能够充分的信任这个工具库?信任依然要有,可完善必要的检查和错误处理势在必行。当我们解决了这个问题,由于它的启发,我们还会联想到其他的问题,比如没有调用回调。
再继续想,你会发现,这样的问题还要好多好多。总结一下可能会出现的问题:
- 回调过早(一般是异步被同步调用);
- 回调过晚或没有回调;
- 回调次数过多;
- 等等
加上了这些检查,强壮之后的代码可能是这样的:
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只能被决议一次,且决议之后无法改变,所以,即便是多次回调,也不会影响结果,决议之后的调用都会被忽略。
参考资料:
更多专业前端知识,请上【猿2048】www.mk2048.com