JS异步编程学习笔记二:Promise演进史

978 阅读6分钟

1、为什么要使用Promise

防止回调嵌套层数过多导致的回调地狱 ,用promise来解决异步问题

例如:

setTimeout(function(){
   //第⼀秒后执⾏的逻辑
   console.log('第⼀秒之后发⽣的事情')
   setTimeout(function(){
      //第⼆秒后执⾏的逻辑
      console.log('第⼆秒之后发⽣的事情')
      setTimeout(function(){
          //第三秒后执⾏的逻辑
         console.log('第三秒之后发⽣的事情')
      },1000)
   },1000)
},1000)
//效果:每隔1秒后输出,内容过多会导致“回调地狱”

2、Promise如何解决异步控制问题

Promise通过链式调用的结构,将原本回调嵌套的异步处理流程变成了.then( ).then( )... 的链式结构。这样虽然仍然离不开回调函数,但是在编程阅读上有了很大改进,更易于维护。

//使⽤Promise拆解的setTimeout流程控制
var p = new Promise(function(resolve){
    setTimeout(function(){
       resolve()
    },1000)
})
p.then(function(){
   //第⼀秒后执⾏的逻辑
    console.log('第⼀秒之后发⽣的事情')
    return new Promise(function(resolve){
      setTimeout(function(){
        resolve()
      },1000)
   })
}).then(function(){
   //第⼆秒后执⾏的逻辑
   console.log('第⼆秒之后发⽣的事情')
   return new Promise(function(resolve){
     setTimeout(function(){
        resolve()
     },1000)
   })
}).then(function(){
   //第三秒后执⾏的逻辑
   console.log('第三秒之后发⽣的事情')
})

3、回调函数

把函数当作变量传递给另一个函数。回调函数本身是同步代码

//把fn当作函数对象那么就可以在test函数中使⽤()执⾏他
function test(fn){
 fn()
}
//那么运⾏test的时候fn也会随着执⾏,所以test中传⼊的匿名函数就会运⾏
test(function(){
 ...
})

js中的回调函数默认是同步结构,但由于js单线程异步模型的规则,要想编写异步的代码,必须使用回调嵌套的形式才能实现。所以:回调函数结构不一定是异步代码,但异步代码一定是回调结构

4、Promise结构

resolve() 和 reject () 只能执行一个,两个都写则后一个不执行;两个都不写则没有输出finally也不会执行

//实例化⼀个Promise对象
var p = new Promise(function(resolve,reject){
  resolve()  //执行then()
  reject()  //执行catch()
})
//通过链式调⽤控制流程
p.then(function(){
   console.log('then执⾏')
}).catch(function(){
   console.log('catch执⾏')
}).finally(function(){
   console.log('finally执⾏')
})

5、Promise链式调用

运行下边代码看结果

//通过⼀个超⻓的链式调⽤我们学习⼀下链式调⽤的注意事项
var p = new Promise(function(resolve,reject){
    resolve('我是Promise的值')
})
console.log(p)
p.then(function(res){
   //该res的结果是resolve传递的参数
   console.log(res)
}).then(function(res){
   //该res的结果是undefined
   console.log(res)
   return '123'
}).then(function(res){
   //该res的结果是123
   console.log(res)
   return new Promise(function(resolve){
     resolve(456)
   })
}).then(function(res){
   //该res的结果是456
   console.log(res)
   return '我是直接返回的结果'
}).then()
.then('我是字符串')
.then(function(res){
   //该res的结果是“我是直接返回的结果”
   console.log(res)
})

根据上面现象得出链式调用的基本形式(参数):

  1. 只要有 then() 并且触发了resolve ,整个链条就会执行到结尾,这个过程中的第一个回调函数的参数就是resolve传递的参数
  2. 后续每个then( )内函数都可以使用 return 返回一个结果,如果没有返回的话,下一个then中回调函数的参数就是undefined
  3. 返回结果如果是普通变量,那么这个值就是 下一个then中接受的参数
  4. 如果返回的是一个Promise对象,那么它的resolve() 就是下一个then中接受的参数、
  5. 如果then中传入的不是函数,或者未传值,Promise链条也不会中断then的链式调用,并且在这之前最后一次的返回结果,会进入下一个离它最近的then中作为参数传递。

6、中断链式调用

有两种形式可以中断.then的链式调用

  • 方式一:抛出一个异常 throw('我是中断的原因:抛出异常')
  • 方式二:返回一个reject的 promise对象
var p = new Promise(function(resolve,reject){
     resolve('我是Promise的值')
})
console.log(p)
p.then(function(res){
    console.log(res)
}).then(function(res){
  //有两种⽅式中断Promise
  //throw('我是中断的原因:抛出异常')
  return Promise.reject('我是中断的原因')
}).then(function(res){
   console.log(res)
}).then(function(res){
   console.log(res)
}).catch(function(err){
   console.log(err)
})

7、小结:

根据以上promise运行时的规则就能解释的通,为什么最初通过Promise控制setTimeout每秒执行一次的代码可以实现。 因为:当我们使用.then()进行链式调用时,可以利用返回一个新的promise对象来执行下一次then函数,而下一次then函数的执行,必须等待其内部resolve的调用。这样我们再new Promise时,放入setTimeout来进行延时,保证1秒之后让状态变更,就不用编写回调嵌套来实现连续的执行异步流程了。

8、Promise常用api

Promise.all()

使用案例: 需要同时调用三个服务端接口接口,且保证三个数据的接口全部返回数据后才能渲染页面。接口a耗时1s, 接口b耗时0.8s,接口c耗时1.4s。

如果.then().then().then()链式调用需要花费的时间是 3.2s 。这种累加显然增加了接口调用的耗时,所以promise 提供了一个all()方法 :

Promise.all([promise对象,promise对象,...]).then(回调函数)

等最慢的接口返回数据一起得到所有接口的数据,这个耗时只会按最慢的接口消耗 1.4 s

let p1 = new Promise((resolve,reject) => {
   setTimeout(() => {
     resolve('第⼀个promise执⾏完毕')
   },1000)
})
let p2 = new Promise((resolve,reject) => {
   setTimeout(() => {
     resolve('第⼆个promise执⾏完毕')
   },800)
})
let p3 = new Promise((resolve,reject) => {
   setTimeout(() => {
     resolve('第三个promise执⾏完毕')
   },1400)
})
Promise.all([p1,p3,p2]).then(res => {
   console.log(res)
}).catch(function(err){
   console.log(err)
})
//结果:res返回一个数组: ['第⼀个promise执⾏完毕','第二个promise执⾏完毕','第三个promise执⾏完毕']

多个promise任务,保证处理的这些所有promise对象的状态全部变成为fulfilled之后才会出发all的.then函数来保证将放置在all中的所有任务的结果返回

Promise.race()

使用案例: 播放某个视频时,有多个播放源,为了保证用户获得较迟的延时,要求使用耗时最短的播放源作为视频的默认播放源。

let p1 = new Promise((resolve,reject) => {
   setTimeout(() => {
     resolve('第⼀个promise执⾏完毕')
   },5000)
})
let p2 = new Promise((resolve,reject) => {
   setTimeout(() => {
     reject('第⼆个promise执⾏完毕')
   },2000)
})
let p3 = new Promise(resolve => {
   setTimeout(() => {
     resolve('第三个promise执⾏完毕')
   },3000)
})
Promise.race([p1,p3,p2]).then(res => {
   console.log(res)
}).catch(function(err){
   console.error(err)
})
//结果:res返回 第⼆个promise执⾏完毕

promise.race()相当于将传⼊的所有任务 进⾏了⼀个竞争,他们之间最先将状态变成fulfilled的那⼀个任务就会直接的触发race的.then函数并且将他的值返回,主要⽤于多个任务之间竞争时使⽤

9、关于Async 和 Await

async 修饰的函数会被解释成promise对象。

运行以下程序查看结果

async function test(){
  console.log(3)
  var a = await 4
  console.log(a)
}
console.log(1)
test()
console.log(2)
//结果: 1  3  2  4

为什么会出现这样的情况呢,其实将async翻译成promise结构就很容易理解了

console.log(1)
new Promise(function(resolve,reject)=>{
    console.log(3)   
    resolve(4)
}).then(function(a){
    console.log(a) 
})
console.log(2)

综上可以看出,async最大的特点,就是第一个await右边和上边的代码都是同步区域,相当于new Promise的回调;第一个await的下边就成了.then中异步执行的代码,await的左边是resolve传递给 then的参数。