es6入门4--promise详解
可以说每个前端开发者都无法避免解决异步问题,尤其是当处理了某个异步调用A后,又要紧接着处理其它逻辑,而最直观的做法就是通过回调函数(当然事件派发也可以)处理,比如:
请求A(function (请求响应A){ //请求响应A作为参数调用方法B funB(请求响应A); });
但从业务角度来说,回调往往不会只有一层;例如我项目中有一个购物车结算的需求:我需要先给网站A下个单,然后以A请求返回的单号为参数调用另一个借口,以给网站B下一个回执单,回执单拿到之后才是跳转页面,大概是这样:
下单A(function (请求响应A){ //下单A响应成功后调用下单B 下单B(function(请求响应B){ //下单B成功后跳转 window.location.href = '我是订单页' }); });
如果请求再多点呢,通过回调的做法我们只能层层嵌套,这也就诞生了让代码维护者头痛的回调地狱。想想几个月后,你的同事或者自己重新阅读这段代码时,函数嵌套与未分离的大量业务代码,不头大都难!
而Promise的出现正好解决了这一痛点,通过promise我们能以链式调用的形式取代传统回调嵌套的写法,同时还能将逻辑代码从毁掉地狱中抽离出来,改写上面上面的例子,像这样是不是好看了很多:
new Promise(下单A) .then(下单B(单号A)) .then(页面跳转)
一、基本用法
Promise强大的地方就在于能通过对异步请求状态的改变,与我们达成一种承诺;例如将请求的状态由pending(进行中)改为fulfilled(已完成),我们就可以通过then方法对应的回调处理相关后续逻辑了。
Promise对象的状态一共有三种,进行中pending,已成功fulfilled(resolved)与已失败rejected。值得一提的是,promise对象的状态不会受外界影响,但我们可以通过内置方法对状态进行改变。且状态一旦改变,此状态就会定型;直白点说就是假设我们将pending改为resolved后,不管什么时候再去访问它,状态将永远维持为已完成状态。我们通过一个例子证实这一点:
let fn = (resolve, reject) => { //改为resolved resolve(1); //再次修改状态无效 reject(2); }; let p = new Promise(fn); //输出1 p.then(resp => console.log(resp), err => console.log(err));
1.创建promise实例并通过then回调
Promise对象是一个构造函数,我们可以通过new来新建一个Promise实例:
function promiseDemo(resolve, reject) { if (true) { //异步请求成功 resolve(1); } else { reject(respError); }; }; let promise = new Promise(promiseDemo); console.log(promise);
在new的操作中,我们需要给Promise传递一个函数作为参数,且此函数中可直接使用resolve与reject参数,用于修改异步请求的状态。
在上述代码中,我们假设发起了一次异步请求,同时拟定true为异步请求成功,通过resolve处理请求成功的返回数据,得到了一个promise实例。通过打印可以看到状态被修改为已成功(resolved),同时得到了响应成功数据value为1;
现在尝试使用then处理回调,可以看到成功输出了1,而这个1是我们拟定异步请求成功返回的数据。
promise.then(function (resp){ //异步请求成功后续逻辑 console.log(resp);//1 },function (resprror){ //请求失败逻辑 });
那上面就是一个模拟的简单的promise例子了,下面具体聊聊promise内置方法,看看promise具体有哪些用法。
二、Promise方法介绍
1.Promise.resolve()
Promise.resolve()能将一个对象转为promise对象。
我在前面说,通过new Promise能得到一个promise实例对象,其实通过Promise.resolve()也能创建一个promise实例,下面这个例子可以看到得到的结果是相同的:
let promise1 = Promise.resolve(1); let promise2 = new Promise(resolve => resolve(1)); console.log(promise1, promise2);
需要注意的是Promise.resolve()接受的参数不同,处理参数的行为也将不同。
1.1传递一个promise对象作为参数
如果我们为Promise.resolve()传递一个promise对象,那么它将会将此对象原封不同的返回:
let promise1 = Promise.resolve(1); let promise2 = Promise.resolve(promise1); console.log(promise1 === promise2);//true 还是原来的味道
当我们使用promise作为参数用于创建另一个promise对象时,还需要注意promise具有状态传递的特性:
const p1 = new Promise((resolve, reject) => { setTimeout(() => reject(1), 3000) }); const p2 = new Promise((resolve, reject) => { setTimeout(() => resolve(p1), 1000) }); //等待四秒后触发reject回调,输出失败了 p2.then(resp => console.log('成功了'),err => console.log('失败了'));
上述代码中,p2的状态由p1决定,四秒后p1 then方法还是触发了失败回调。
1.2传递一个非对象,例如数字,字符串
当我们传递一个非对象作为参数,Promise.resolve会返回一个promise对象,状态为resolved,且通过then回调我们能正常访问该参数。
let promise1 = Promise.resolve(1);//第一步执行 console.log(promise1);//第二步执行 promise1.then(resp => console.log(resp));//then方法最后执行 console.log(2);//第三步执行
在ES6入门这本书中说,由于传递的参数不具备异步行为(不带有then方法),所以Promise.resove同步执行修改了参数状态,并立刻执行then回调;但事实并非如此;如上,通过断点将执行先后步骤加在了注释里,打印2的操作要早于then,then依旧是最后执行,这里特别指出。
我对于这里的理解是,不管是new Promise()中的resolve()还是Promise.resolve(),只要不具备异步行为,resolve方法本身都将同步执行,但是then回调仍然是异步触发。
一个new Promise例子:
let demo = resolve => { resolve(1); }; let promise = new Promise(demo); //此时的promise状态已修改为resolved,说明resolve(1)已触发 console.log(promise) promise.then(resp => { console.log(resp); }); console.log(2);
这个例子中依次打印promise,2,1。且打印出的promise对象状态为resolved,说明resolve()方法为同步执行,但then回调最后触发。
一个Promise.resolve()例子:
let promise = Promise.resolve(1);
console.log(promise);
promise.then(resp => console.log(resp))
console.log(2);
依旧是第一打印promise,第二次打印1,且promise状态是resolved,说明在打印1之前,Promise.resolve(1)已经执行完成,then回调最后触发执行。
这里我加个例子与上面的代码做对比,让resolve处理异步操作,下面的代码才符合resolve完成立刻触发then回调的情况:
let demo = resolve => { console.log(1); setTimeout(() => { resolve(4); }, 1000); console.log(2); }; let promise = new Promise(demo); promise.then(resp => console.log(resp)); console.log(3);
上述代码依次会输出1,2,3,4,这个例子中resolve()方法由于异步的问题等到同步代码跑完了才触发了,同时resolve完成立刻触发了then方法,最后输出了4。
1.3.传递一个thenable对象
thenable对象是指带有then方法的对象,我们可以手动创建此类对象,我个人感觉angular中$http返回的对象应该也是thenable对象。
对于thenable对象resolve方法会将此对象转为promise对象,得到的实例也能正常通过then方法回调。
let thenable = { then: (resolve, reject) => resolve(42) }; let p1 = Promise.resolve(thenable); p1.then(value => console.log(value));//42
1.4.不传递参数
如果不传递参数,则得到一个没有value,但状态是resolved的promise对象。
let promise = Promise.resolve();
console.log(promise);
2.Promise.reject()
Promise.reject()也会返回一个promise实例,状态为rejected。reject接收的参数会原封不动作为reject回调时的参数,不像resolve那么多情况。
let thenable = { then: function (resolve, reject) { reject(1); } }; let p = Promise.reject(thenable); p.then(resp => { console.log(resp) }, err => { console.log(err === thenable)//true });
3.Promise.prototype.then()
为什么次方法是Promise.prototype.then()而不是Promise.then()呢,这是因为then方法是为Promise实例提供,而实例的方法是通过继承而来,then方法在Promise对象的原型链上也就合情合理了。
通过前面的例子,我们也知道了then方法提供了2个回调函数,第一个对应resolved状态,第二个对应rejected状态。
需要注意的是,then方法会隐性返回一个新的promise实例,我们甚至可以无限使用then回调都不会报错:
let p = Promise.resolve(1); p.then(resp => console.log(resp)) //1 .then(resp => console.log(resp)) //undefined .then(resp => console.log(resp)) //undefined // ...无数个
也正因为这个特性,我们在处理异步请求A的then回调中,可以手动返回一个异步请求B的promise实例,通过这样的做法也就实现了同步链式的写法:
let p1 = Promise.resolve(1), p2 = Promise.resolve(2), p3 = Promise.resolve(3); p1.then(resp => { return p2; }).then(resp => { return p3; }).then(resp => { console.log(resp);//3 });
4.Promise.prototype.catch()
总是推荐使用catch()方法代替then方法中的第二个回调;这是因为catch方法不仅能捕获异步请求的错误,它还能捕获then方法的错误,但then的第二个回调做不到这一点。
let p = Promise.resolve(1); p.then(resp => { console.log(x); }).catch(err => { console.log(err);//x is not defined });
上述代码中我们在成功回调中故意打印一个未定义的变量x,catch成功帮我们捕获了这个错误,但是如果使用then第二个错误回调,是无法捕获的。
const fn = (resolve,reject) => { console.log(x); }; let p = new Promise(fn); p.then(resp => { console.log(1); }).catch(err => { console.log(err);//x is not defined });
这个例子中,我们在创建promise实例的函数中故意出错,catch也捕获了错误,虽然这个错误then第二个回调也能做到,但整体来说catch更为强大,这也是推荐使用catch而不是then第二个回调的理由。
说到处理错误,Promise还有个奇怪的地方,假设Promise出错了,但没使用then第二回调或者catch处理错误;尽管程序会报错,但这个错误并不会抛出给外层,所以外层程序并不会因此停止执行,所以在then回调后面跟一个catch方法是有必要的。
const fn = (resolve, reject) => { resolve(1); }; let p = new Promise(fn); p.then(resp => { console.log(x); }); setTimeout(() => { console.log(1); }, 0);
5.Promise.prototype.finally()
finally()方法有点像switch case中的default,不管你异步成功了还是失败了,finally都会如约而至的触发。一般用法是这样:
const p = Promise.resolve(1); p.then(resp => console.log(resp)) .catch(err => console.log(err)) .finally(() => console.log('执行完毕'));
finally方法因为不关心Promise状态,所以不需要传递参数,在ES6入门中也提到,由于不管状态成功或者失败都会触发,所以finally也等同于then方法中使用两个回调的做法。
6.Promise.all()
Promise.all()方法接受多个promise实例,返回一个全新的promise实例:
let p = Promise.all([p1, p2, p3]);
如果p1,p2,p3不是promise实例,则会在all执行前先为这三个参数执行Promise.resolve()方法。
all方法返回的promise实例的状态由参数共同决定,以上面代码为例,如果三个promise实例状态全部为resolved,则p的状态便为resolved,但如果三个实例有一个未rejected,则p的状态便为rejected。
//全部为resolved let p1 = Promise.resolve(1), p2 = Promise.resolve(2), p3 = Promise.resolve(3); let p = Promise.all([p1, p2, p3]); p.then(resp => console.log(resp));//[1,2,3]; //部分为reject let p1 = Promise.resolve(1), p2 = Promise.reject(2), p3 = Promise.reject(3); let p = Promise.all([p1, p2, p3]); p.then(resp => console.log(resp)) .catch((err) => console.log(err));//2;
这里有个需要注意的地方,如果一个状态为rejected的promise实例使用了then方法的第二回调,或者使用了catch()方法,这会导致all()无法触发自己catch()方法或者then的第二回调。
let p = Promise.reject(1).then(resp => resp, err => err); // 或者 // let p = Promise.reject(1).then(resp => resp) // .catch(err => err); Promise.all([p]) .then(resp => console.log('成功执行'))//成功执行 .catch(err => console.log('报错啦'));
上述例子中创建promise对象时虽然使用了reject方法,但由于自身有捕获错误的操作,导致实例p拿到的是then方法返回的另一个promise对象。我们可以看看状态:
let p = Promise.reject(1).then(resp => resp, err => err);
console.log(p);
不管什么时候都应该记住,then方法也会返回一个新的promise对象。
7.Promise.race()
Promise.race()同样是接受多个promise实例返回一个全新promise实例的方法,但与all方法不同的地方在于,决定这个promise状态的是多个实例参数中最先改变状态的那个。
let p = Promise.race([p1, p2, p3]);
假设p3最先改变状态成了rejected,那么p的状态也就是rejected。p1,p2随后再改变状态将不会对p实例起作用。
let p1 = Promise.reject(1); let p2 = Promise.resolve(resole => { setTimeout(() => { resole(2); }, 3000) }); Promise.race([p1, p2]) .then(resp => console.log(resp)) .catch(err => console.log(err));//1
上述例子中p1是一个同步执行状态为rejected的promise实例,p2是异步创建状态为resolved的实例,由于p2需要等三秒,所以最终race的实例以p1为主,这里最终输出1。
那么到这里promise基本方法就介绍完了,一个个例子去理解以及自己钻牛角尖也花了一点时间,接下来应该会写下promise执行顺序的文章,这个与宏任务微任务挂钩,还有就是手写promise需要看下,那么这篇文章就写到这里。
如果对于JS执行机制相关有疑问,可以阅读博主这篇博文
欢迎大家留言讨论!!!