相信凡是写过javascript的童鞋也一定都写过回调方法(callback),简单说回调方法就是将一个方法func2作为参数传入另一个方法func1中,当func1执行到某一步或者满足某种条件的时候才执行传入的参数func2,例如下面的代码段
// 当参数a大于10且参数func2是一个方法时 执行func2 function func1(a, func2) { if (a > 10 && typeof func2 == 'function') { func2() } } func1(11, function() { console.log('this is a callback') })
一般来说我们会碰到的回调嵌套都不会很多,一般就一到两级,但是某些情况下,回调嵌套很多时,代码就会非常繁琐,会给我们的编程带来很多的麻烦,这种情况俗称——回调地狱。极端情况如下图:
由此,Promise的概念就由社区提出并实现,作用与回调方法几乎一致,都是在某种情况下执行预先设定好的方法,但是使用它却能够让代码变得更简洁清晰
什么是Promise
Promise是异步编程的一种解决方案,它有三种状态,分别是pending-进行中、resolved-已完成、rejected-已失败
当Promise的状态又pending转变为resolved或rejected时,会执行相应的方法,并且状态一旦改变,就无法再次改变状态,这也是它名字promise-承诺的由来
ES6 Promise 先拉出来遛遛
复杂的概念先不讲,我们先简单粗暴地把Promise用一下,有个直观感受。那么第一个问题来了,Promise是什么玩意呢?是一个类?对象?数组?函数?
这么一看就明白了,Promise是一个构造函数,自己身上有all、reject、resolve这几个眼熟的方法,原型上有then、catch等同样很眼熟的方法。这么说用Promise new出来的对象肯定就有then、catch方法喽,没错。
那就new一个玩玩吧。
var p = new Promise(function(resolve, reject){ //做一些异步操作 setTimeout(function(){ console.log('执行完成'); resolve('随便什么数据'); }, 2000); });
Promise的构造函数接收一个参数,是函数,并且传入两个参数:resolve,reject,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数。其实这里用“成功”和“失败”来描述并不准确,按照标准来讲,resolve是将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected。不过在我们开始阶段可以先这么理解,后面再细究概念。
在上面的代码中,我们执行了一个异步操作,也就是setTimeout,2秒后,输出“执行完成”,并且调用resolve方法。
运行代码,会在2秒后输出“执行完成”。注意!我只是new了一个对象,并没有调用它,我们传进去的函数就已经执行了,这是需要注意的一个细节。所以我们用Promise的时候一般是包在一个函数中,在需要的时候去运行这个函数,如:
function runAsync(){ var p = new Promise(function(resolve, reject){ //做一些异步操作 setTimeout(function(){ console.log('执行完成'); resolve('随便什么数据'); }, 2000); }); return p; } runAsync()
runAsync().then(function(data){ console.log(data); //后面可以用传过来的数据做些其他操作 //...... });
function runAsync(callback){ setTimeout(function(){ console.log('执行完成'); callback('随便什么数据'); }, 2000); } runAsync(function(data){ console.log(data); });
效果也是一样的,还费劲用Promise干嘛。那么问题来了,有多层回调该怎么办?如果callback也是一个异步操作,而且执行完后也需要有相应的回调函数,该怎么办呢?总不能再定义一个callback2,然后给callback传进去吧。而Promise的优势在于,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。
链式操作的用法
所以,从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。所以使用Promise的正确场景是这样的:
runAsync1() .then(function(data){ console.log(data); return runAsync2(); }) .then(function(data){ console.log(data); return runAsync3(); }) .then(function(data){ console.log(data); });
function runAsync1(){ var p = new Promise(function(resolve, reject){ //做一些异步操作 setTimeout(function(){ console.log('异步任务1执行完成'); resolve('随便什么数据1'); }, 1000); }); return p; } function runAsync2(){ var p = new Promise(function(resolve, reject){ //做一些异步操作 setTimeout(function(){ console.log('异步任务2执行完成'); resolve('随便什么数据2'); }, 2000); }); return p; } function runAsync3(){ var p = new Promise(function(resolve, reject){ //做一些异步操作 setTimeout(function(){ console.log('异步任务3执行完成'); resolve('随便什么数据3'); }, 2000); }); return p; }
在then方法中,你也可以直接return数据而不是Promise对象,在后面的then中就可以接收到数据了,比如我们把上面的代码修改成这样:
runAsync1() .then(function(data){ console.log(data); return runAsync2(); }) .then(function(data){ console.log(data); return '直接返回数据'; //这里直接返回数据 }) .then(function(data){ console.log(data); });
reject的用法
function getNumber(){ var p = new Promise(function(resolve, reject){ //做一些异步操作 setTimeout(function(){ var num = Math.ceil(Math.random()*10); //生成1-10的随机数 if(num<=5){ resolve(num); } else{ reject('数字太大了'); } }, 2000); }); return p; } getNumber() .then( function(data){ console.log('resolved'); console.log(data); }, function(reason, data){ console.log('rejected'); console.log(reason); } );
catch的用法
我们知道Promise对象除了then方法,还有一个catch方法,它是做什么用的呢?其实它和then的第二个参数一样,用来指定reject的回调,用法是这样:
getNumber() .then(function(data){ console.log('resolved'); console.log(data); }) .catch(function(reason){ console.log('rejected'); console.log(reason); });
效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。请看下面的代码:
getNumber() .then(function(data){ console.log('resolved'); console.log(data); console.log(somedata); //此处的somedata未定义 }) .catch(function(reason){ console.log('rejected'); console.log(reason); });
all的用法
Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。我们仍旧使用上面定义好的runAsync1、runAsync2、runAsync3这三个函数,看下面的例子:
Promise .all([runAsync1(), runAsync2(), runAsync3()]) .then(function(results){ console.log(results); });
race的用法
all方法的效果实际上是「谁跑的慢,以谁为准执行回调」,那么相对的就有另一个方法「谁跑的快,以谁为准执行回调」,这就是race方法,这个词本来就是赛跑的意思。race的用法与all一样,我们把上面runAsync1的延时改为1秒来看一下:
Promise .race([runAsync1(), runAsync2(), runAsync3()]) .then(function(results){ console.log(results); });
//请求某个图片资源 function requestImg(){ var p = new Promise(function(resolve, reject){ var img = new Image(); img.onload = function(){ resolve(img); } img.src = 'xxxxxx'; }); return p; } //延时函数,用于给请求计时 function timeout(){ var p = new Promise(function(resolve, reject){ setTimeout(function(){ reject('图片请求超时'); }, 5000); }); return p; } Promise .race([requestImg(), timeout()]) .then(function(results){ console.log(results); }) .catch(function(reason){ console.log(reason); });