此文章同步发表在我的博客:我的博客
前言
在js领域,promise出现的时间已经很久了,从jquery的$.get().done().fail() 这样的API开始,到现在的es6默认支持的new Promise(),它的出现无疑使异步代码变得信任可靠,更使得前端代码的信任度直线提升,就跟它的名字一样,它的到来,是一个 Promise (保证,允诺)
异步信任问题
有一个朋友在银行系统工作,有一次,我帮他review一段代码,据他所说,这段代码给他带来了一个很大的麻烦,有一位顾客使用这个页面付款的时候意外的提交了两次付款请求,boss复查发现提交的源头是前端,当我接到这段代码的时候,这位朋友已经焦头烂额,我接过了代码, 初步分析,什么也没发现,他的代码就像下面这样:
$("btn").on("click",function(){
if($(this).hasClass("no-submit")){
$(this).removeClass("no-submit");
//忽略
$.ajax({
url:"....",
type:"json",
success:function(){
submit();
}
})
}
})
他告诉我,他已经针对按钮做点击唯一提交了,怎么可能调用两次?
但是事实就是有两次submit。
通过分析代码,最后排查的结果,原因是这个jquery的$.ajax是被二次封装过的,某种情况下回调函数被调用了两次。
oh,看起来很简单的原因,这可是个巨坑! 日常代码中,我们都需要接触各种各样的第三方库,谁来保证它们都没有隐藏的问题?如果你遇到了经过加密混淆的库,因为它内部某个try catch,或者内部某个if在非正常情况下发成的误判导致重复调用,我想你就不会再淡定的喝着咖啡敲代码了。
或者你觉得,防止重复调用这个很简单,加个判断就行了:
var a = 0;
$("btn").on("click",function(){
if($(this).hasClass("no-submit")){
$(this).removeClass("no-submit");
//忽略
$.ajax({
url:"....",
type:"json",
success:function(){
if( a==0){
submit();
}
}
})
}
})
完美是吧,测试上线,继续喝咖啡。
三个月后。。。。。
有些顾客反应,点击提交后,有时没有提交成功,老板这时大发雷霆,这已经是第二次出现这样的失误了。
是的,只调用一次,但是鬼知道它会不会调用一次?
这里问题的本质是什么?
它就是所谓的回调地域。
我们总是有很多的回调函数需要交给第三方工具处理,类似于$.ajax,jquery就一定是值得信任的?图样图森破!你永远不知道jquery下面可能还埋着上一位开发者给你准备的周年大礼包。最糟糕的是,这个第三方工具你没有修改的能力,要么你换掉他,要么你解密,要么换版本,谁知道作者会不会继续更新?
把我们的回调交给不受信任的第三方程序,这就是回调地域,可怕的很!
Promise调用方式
今天的主角是promise。它可以让我们的回调变得promise。
试想,我们把上述的ajax再次封装(在不能改动源代码的情况下),可能代码如下:
var then = function(resolve,reject,timeout){
var a = 0;
$("btn").on("click",function(){
if($(this).hasClass("no-submit")){
$(this).removeClass("no-submit");
//忽略
$.ajax({
url:"....",
type:"json",
success:function(){
a==0&&resolve();
a++;
},
error:function(){
a==0&&reject();
a++;
}
})
}
})
if(new Date.now()>timeout){
a==0&&reject();
a++;
}
}
var a = 0;
ok,我只是将前面的代码封装到一个叫then的函数里,这个函数需要三个参数,resolve(解决),reject(拒绝),以及一个超时时间,另外控制调用次数的变量a也有保留。
有什么不同?
不同之处在于,then函数是可信任的,至少现在我们可以确定,它要么被resolve,要么reject,绝不可能出现调用两次,或者不调用的情况。这种封装可以叫做控制反转,
我们将原本给到$.ajax的回调权再次交回自己手中,上面的代码调用起来是这样的:
then(function(){
submit()
},function(){
error()
})
ok,我们只解决了其中的一些问题,重复调用,不调用,但还有更多的问题。
让promise登场吧!
使用promise封装上面代码:
function request(){
return new Promise(function(resolve,reject){
$.ajax({
url:"....",
type:"json",
success:function(){
resolve()
},
error:function(){
reject()
}
})
})
}
request.then(function(){
submit();
},function(){
error();
})
ok,是否豁然开朗?
promise就是解决关于异步回调信任,顺便缓解了回调金字塔(一堆回调嵌套在一起,就是一个回调金字塔)问题的解决方案。
在这个Promise构造函数中传入一个函数,这个函数有两个值,resolve、reject。一旦代码请求成功,resolve被调用,决议完成并忽略后面的所有调用。或者失败后决议被拒绝,调用reject。
决议、解决、拒绝术语
一个promise通常有三种状态 pending(进行中)、fulfilled(已成功)和rejected(已失败)。
可以说在任何情况下,一个promise只有一个决议。 它只可能是fulfilled或者rejected其中一种,并且是不可以逆转的。意思是你无法让一个已经被拒绝的promise回滚。它永远都会有一个确定的回复。
Promise API
promise.all(Array);
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例,并且这个新的promise返回的最终值是传入的多个Promise决议完成的值。
有了Promise.all方法,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加简单易懂。
像这样的调用:
Promise.all([request("url1"),request("url2")]).then((result)=>{
console.log(result) //[url1result,url2result]
})
注意传入的顺序,数组promise决议的值与返回的result数组位置是一致的。对于以往的回调方式来说,这是很强大的api。
Promise.race(array)
这个方法和all调用方式一样,但是决议的方式不一样,.all等待所有决议完成新的 Promise返回决议。 而.race,一旦有某个promise决议完成,其余的会立刻终止。也就是说它只会执行最快的那个promise。
阮一峰的博客…更多api可以查看阮大大的es6教程
结束
promise已经成为主流。我在这里只讲了一些浅见,至于更多的用法,与君共勉!
时代在进步,async await已然成为主流
@lpbird 似乎async返回的是promise对象吧
是的,只是看到文中的示例,介绍更加简洁,更容易错误处理的es7 会更好
你们都不用async.auto吗? 这个也很好用啊