浅谈Promise原理与应用
在JavaScript中,所有代码都是单线程。由于该“缺陷”,JavaScript在处理网络操作、事件操作时都是需要进行异步执行的。AJAX就是一个典型的异步操作
对于异步操作,有传统的利用回调函数和使用 Promise,二者的对比如下:
//以往回调方式 函数1(function(){ //代码执行...(ajax1) 函数2(function(){ //代码执行...(ajax2) 函数3(function(data3){ //代码执行...(ajax3) }); ... }); }); //Promise回调方式:链式调用,可构建多个回调函数。 //例如请求一个ajax之后,需要这个拿到这个ajax的数据去请求下一个ajax promise().then().then()...catch()
对比可知,使用传统回调函数方式处理异步操作很复杂。为了解决这样的问题,commonJS引入了Promise概念,很好的解决了JavaScript的异步操作。
概念
Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更简单易理解且实用,所以Promise简单来说就是一个容器,里面保存着某个未来才会执行事件(通常为一个异步操作)的结果。
特点
- 对象的状态不受外界影响
- 一旦状态改变,就不会再变化,任何时候都可以得到这个结果
语法
//创建Promise实例 let promise = new Promise( (resolve,reject) =>{ // 执行相应代码并根据情况调用resolve或者reject ... } ) // 在promise的then方法中执行回调 Promise.then( function(){ // 第一个参数是返回resolve状态时执行的回调函数 },function(){ // 第二个参数是返回reject状态时执行的回调函数 } )
状态
Promise对象代表一个异步操作,有三种状态
pending(等待)、resolved(成功状态)、rejected(失败状态)
两种状态改变方式:pending => resolved,pending => rejected
注:只有异步操作的结果才可以决定当前是哪一种状态,任何其他操作都无法改变这个状态
缺点
- 无法取消 Promise,一旦新建他就会立即执行,中途无法取消;
- 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部;
- 当处于 pending 状态时,无法得知目前进展到哪一阶段(刚刚开始还是即将完成)
用法 先看下面这个例子:
1 setTimeout( ()=>{ 2 console.log('123'); 3 },0 ) 4 console.log('456');
执行结果相信大家都知道,上面console处于异步代码中(即使延迟为0),下面console处于同步代码中,如果想要 ‘456’ 在 ‘123’ 执行结束后再输出呢?
传统回调函数方式:
1 setTimeout( ()=>{ 2 console.log('123'); 3 fn(); 4 },0 ) 5 function fn(){ 6 console.log('456') 7 }
8 // 123
9 // 456
使用 Promise 方式:
1 let promise = new Promise( (resolve,reject) =>{ 2 setTimeout( ()=>{ 3 console.log('123'); 4 resolve('456'); 5 } ,0) 6 } ) 7 promise.then(function(data){ 8 // resolve状态 9 console.log(data); 10 },function(error){ 11 // reject状态 12 })
13 // 123
14 // 456
Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。也就是说,状态由实例化时的参数(两个不同状态的回调函数)执行来决定的,根据不同的
状态来执行具体哪个函数。
注:resolve() 和 reject() 的阐述会传递到对应的回调函数的 data 或者 error,且 then 返回的是一个新的 Promise 实例,也就是或还可以继续 then
链式操作用法 从 表面行看,我们或许会觉得 Promise 只是能够简化传统的层层回调写法。然而,Promise的精髓是 ‘状态’,用维护状态,传递状态的方式来使得回调函数能够及时调用,它比传递 callback 函数要灵活、简单的多。下面为一个 Promise 的一般使用场景:
1 async1() 2 .then(function(data){ 3 console.log(data); 4 return async2(); 5 }) 6 .then(function(data){ 7 console.log(data); 8 return async3(); 9 }) 10 .then(function(data){ 11 console.log(data); 12 }); 13 14 function async1(){ 15 let p = new Promise(function(resolve,reject){ 16 // 异步操作 17 setTimeout(()=>{ 18 console.log('异步任务1执行完成!'); 19 resolve('数据1'); 20 },1000); 21 }); 22 return p; 23 }; 24 function async2(){ 25 let p = new Promise((resolve,reject)=>{ 26 // 异步操作 27 setTimeout(()=>{ 28 console.log('异步任务2执行完成!'); 29 resolve('数据2'); 30 },2000); 31 }); 32 return p; 33 }; 34 function async3(){ 35 let p = new Promise((resolve,reject)=>{ 36 // 异步操作 37 setTimeout(()=>{ 38 console.log('异步任务3执行完成!'); 39 resolve('数据3'); 40 },3000); 41 }); 42 return p; 43 } 44 // 1秒后... 45 // 异步任务1执行完成! 46 // 数据1 47 // 2秒后... 48 // 异步任务2执行完成! 49 // 数据2 50 // 3秒后... 51 // 异步任务3执行完成! 52 // 数据3
在 then 方法中,可以不用 return Promise实例对象,直接返回数据在后面的 then 中也能够接收到数据
reject用法 在前面的例子中只有 resolve(成功状态)的回调,实际应用中还会有失败状态,reject 就是把 Promise 的状态设置为 rejected ,这样我们就能够在 then 中捕捉到,然后执行相应的回调
1 let num = 10; 2 let p1 = function(){ 3 return new Promise((resolve,reject)=>{ 4 if(num <= 5){ 5 resolve("<=5,走resolve"); 6 console.log("resolve不能结束Promise"); 7 } 8 else{ 9 reject(">5,走reject"); 10 console.log("reject不能结束Promise") 11 } 12 }) 13 } 14 15 p1() 16 .then(function(data){ 17 console.log(data) 18 },function(error){ 19 console.log(error) 20 }) 21 // reject不能结束Promise 22 // >5,走reject
resolve 和 reject 永远在当前环境的最后面执行,所以后面的同步代码会先执行
如果 resolve 和 reject 之后还有代码需要执行,最好放在 then 里,然后在 resolve 和 reject 前面写上 return
Promise.prototype.catch() Promise.prototype.catch() 方法是 .then( null, rejection ) 的别名,用于指定发生错误的回调函数
1 p1() 2 .then(function(data){ 3 console.log(data) 4 }) 5 .catch(function(err){ 6 console.log(err) 7 }) 8 //reject不能结束Promise 9 //>5,走reject
Promise.all()
Promise.all() 方法用于将多个 Promise 实例包装成一个新的 Promise 实例
1 const p = Promise.all( [p1,p2,p3] );
p 的状态由 p1、p2、p3 决定,分为两种情况:
- 只有 p1、p2、p3 的状态都为 resolved 时,p 状态才会变成 resolved,此时 p1、p2、p3 的返回值组成一个数组传递给 p 的回调函数
- 只要 p1、p2、p3之中有一个状态为 rejected ,p 的状态就变成 rejected,此时第一个状态 为 rejected 的实例的返回值会传递给 p 的回调函数
由于p 是包含3个 Promise 实例的数组,只有这三个实例状态都为 resolved,或者其中有一个及以上的实例状态为 rejected 时,才会调用 Promise.all 方法后面的回调函数
如果作为参数的 Promise 实例自己定义了 catch 方法,那么它一旦被 rejected,并不会触发 Promise.all 的 catch 方法,如果没有实例参数定义自己的 catch,就会调用 Promise.all 的 catch 方法
Promise.race() Promise.race() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例
1 const p = Promise.race( [p1,p2,p3] )
使用该方法时,只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就会跟着该实例变化,该实例的返回值传递给 p 的回调函数
Promise.resolve() 在实际应用中有时需要将一个对象转为 Promise 对象,Promise.resolve() 方法就能够实现,该实例的状态为 resolved
const promise = Promise.resolve( '123' ); // 等价于 Promise.resolve( '123' ); new Promise( resolve => resolve( '123' ) )
Promise.resolve 方法的参数类型有四种情况:
- 参数为一个 Promise 实例
- 如果参数就是 Promise 实例,那么 Promise.resolve 将不做任何修改,依然返回该实例
- 参数为一个 thenable 对象
- thenable 对象指具有 then 方法的对象,如:
let thenable = { then: function( resolve,reject ){ resolve( 'aaa' ); } }
- Promise.resolve 方法会将这个对象转为 Promise 对象,然后立即执行 thenable 对象的 then 方法
let thenable = { then: function(resolve, reject) { resolve( 'aaa' ); } }; let p1 = Promise.resolve(thenable); p1.then(function(value) { console.log(value); // aaa })
上面代码中,thenable 对象的 then 方法执行后,对象 p1 的状态就变为 resolved,从而立即执行后面那个 then 方法指定的回调函数,输出 aaa
- thenable 对象指具有 then 方法的对象,如:
- 参数不是具有 then 方法的对象,或者根本不是一个对象
- 如果参数是一个原始值,或者不是一个具有 then 方法的对象,则 Promise.resolve 方法返回一个新的 Promise 对象,状态为 resolved
1 const p = Promise.resolve('Hello'); 2 3 p.then(function (s){ 4 console.log(s) 5 }); 6 // Hello
上面代码生成一个新的 Promise 对象的实例 p,由于字符串 Hello 不属于异步操作( String 对象不具有 then 方法),返回 Promise 实例的状态生成就为 resolved,所以回调函数会立即执行,且 Promise.resolve 方法的参数会同时传给回调函数
- 如果参数是一个原始值,或者不是一个具有 then 方法的对象,则 Promise.resolve 方法返回一个新的 Promise 对象,状态为 resolved
- 参数为空
- Promise.resolve 方法可以不带参数使用,此时直接返回一个 resolve 状态的 Promise 对象
- 如果希望得到一个 Promise 对象,比较方便的方法就是直接调用不带参数的 Promise.resolve 方法
1 const p = Promise.resolve(); 2 3 p.then(function () { 4 // ... 5 });
-
上面变量 p 就是一个 Promise 对象,注:立即 resolve 的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时
1 setTimeout(function () { 2 console.log('three'); 3 }, 0); 4 5 Promise.resolve().then(function () { 6 console.log('two'); 7 }); 8 9 console.log('one'); 10 11 // one 12 // two 13 // three
上面代码中,setTimeout(fn,0)在下一轮“事件循环”开始时执行,Promise。resolve() 在本轮执行,console.log('one')则是立即执行,因此最先输出
Promise.reject()
Promise.reject( reason ) 方法也会返回一个新的 Promise 实例,该实例的状态为 rejected
1 const p = Promise.reject('出错了'); 2 // 等同于 3 const p = new Promise((resolve, reject) => reject('出错了')) 4 5 p.then(null, function (s) { 6 console.log(s) 7 }); 8 // 出错了
上面代码生成一个 Promise 对象的实例 p,状态为rejected,回调函数会立即执行
注:Promise.reject() 方法的参数,会原封不动的作为 reject 的理由,变成后续方法的参数。这一点与 Promise.resolve 方法不一致
1 const thenable = { 2 then(resolve, reject) { 3 reject('出错了'); 4 } 5 }; 6 7 Promise.reject(thenable) 8 .catch(e => { 9 console.log(e === thenable) 10 }) 11 // true
上面代码中, Promise.reject 方法的参数为一个 thenable 对象,执行以后,后面 catch 方法的参数不是 reject 抛出的 ‘出错了’ 这个字符串,而是 thenable 对象
两个附加的方法
ES6 的 Promise 提供的 API 不是很多,有些有用的方法可以自己部署,下面介绍两个不在 ES6 中,但很有用的方法
done()
Promise 对象的回调链,不管以 then 方法或者 catch 方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(Promise内部的错误不会冒泡到全局)。因此,可以提供一个 done 方法,总是处于回调链的末端,保证抛出任何可能出现的错误被捕捉
1 asyncFunc() 2 .then(f1) 3 .catch(r1) 4 .then(f2) 5 .done();
实例:
1 Promise.prototype.done = function (onFulfilled, onRejected) { 2 this.then(onFulfilled, onRejected) 3 .catch(function (reason) { 4 // 抛出一个全局错误 5 setTimeout(() => { throw reason }, 0); 6 }); 7 };
上面代码可见,done 方法的使用,可以像 then 方法那样用,提供 resolved 和 rejected 状态的回调函数,也可以不提供任何参数。总之,done 都能够捕捉到任何可能出现的错误,并向全局抛出
finally()
finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。它与 done 方法最大的区别是 finally 方法能够接收一个普通的回调函数作为参数,该函数不管怎样都必须执行
实例:服务器使用 Promise 处理请求,然后使用 finally 方法关掉服务器
1 server.listen(0) 2 .then(function () { 3 // run test 4 }) 5 .finally(server.stop);
1 Promise.prototype.finally = function (callback) { 2 let P = this.constructor; 3 return this.then( 4 value => P.resolve(callback()).then(() => value), 5 reason => P.resolve(callback()).then(() => { throw reason }) 6 ); 7 };
上面代码中,不管前面的 Promise 是resolved 状态还是 rejected 状态,都会执行回调函数callback