Promise基础知识

953 阅读15分钟

一. Promise的含义

  • Promise是异步编程的一种解决方案,比传统的解决方案:回调函数和事件更加合理和强大
  • Promise其实就是一个容器,里面保存着未来才会结束的某个事件(通常是异步操作)
  • Promise对象有以下两个特点:
  1. 对象的状态不受外界影响。Promise对象代表的是异步操作,有三种状态,pending进行中,fulfilled已成功,rejected已失败
  • 只有异步操作的结果才能改变状态,这也是Promise承诺,名字的由来
  1. 一旦状态改变就不会再改变,任何时候都可以得到这个结果。
  • Promise对象的状态改变情况:(1) pending改变为fulfilled;pending改变为rejected;
  • 只要改变为fulfilled或者rejected,那么状态就凝固了,不会再改变,此时被称为resolved已定型
  • 但是一般resolved都是特指fulfilled状态,而不包括rejected状态
  1. Promise的一些缺点: (1) 无法取消Promise,一旦新建就会立即执行,无法中途取消 (2) 如果不设置回调函数,那么Promise内部抛出的错误不会反映到外部 (3) 当处于pending状态时,无法得知目前发展到哪个阶段刚刚开始还是即将完成

二. 基本用法

1. 基本含义

  • ES6规定,Promise对象是一个构造函数,可以用来生成Promise实例,new Promise()的参数函数中有两个参数,调用第一个参数函数会改变状态为resolved状态,调用第二个会改变状态为rejected状态
  • Promise实例生成之后,可以使用then方法指定resolved,rejected状态的回调函数,第一个参数函数在resolved状态时调用,第二个参数在rejected状态时调用
  • 下面是一个Promise实例:
  var p=new Promise(function(res,rej){
   console.log("刚刚new Promise()就会执行,即使是赋值操作")
   // res();// 状态改为resolved
   rej();// 状态改为rejected
  })
  // 获取状态
  p.then(()=>{
   console.log('状态改变为resolved')
  },()=>{
   console.log('状态改变为rejected')
  })

2. setTimeout和promise实例

  function pro(time){
   return new Promise((res,rej)=>{
    // new Promise之后就执行,res会在time时间后执行,从而状态改变为resolved
    setTimeout(res,time)
   })
  }
  console.log(Math.floor(new Date().getTime()/1000))
  pro(1000).then(()=>{
   console.log(Math.floor(new Date().getTime()/1000))
   console.log("在1000ms后执行")
  })

3. Promise异步结合同步实例

  let p=new Promise(function(res,rej){
   console.log("new Promise代码执行,会立即执行该步骤")
   console.log(1);
   res()
  })
  p.then(()=>{
   console.log("then操作时异步操作,会在本轮宏任务结束之前才执行")
   console.log(3)
  })
  console.log(2);

4. ajax请求实例

  var get=function(url){
   return new Promise((res,rej)=>{
    var xhr=new XMLHttpRequest();
    xhr.open("GET",url);
    xhr.onreadystatechange=function(){
     if(this.readyState==4&&this.status==200){
      res(this.response)
     }
    }
    xhr.onerror=(err)=>{
     rej(err)
    }
    xhr.send();
   })
  }
  // http://127.0.0.1:8849,此时会报错(因为本地没开启该端口)
  get("http://127.0.0.1:8848/MyHxsj/面经.md").then((data)=>{
   console.log(data)
  },(err)=>{ console.log(err) })

5. resolve返回一个Promise实例

  • 如果调用resolve函数或者reject函数返回时带有参数。
  • 那么如果resolve函数的参数是一个Promise实例时,返回的实例的状态决定了当前Promise实例的状态
  • 如果返回的Promise实例的状态是pending,那么当前Promise实例会等待pending状态改变
  • 如果返回的Promise实例的状态是resolved或者rejected,那么回调函数就会立即执行
  // 1. 返回的Promise实例的状态为pending
  var p1=new Promise(function(res,rej){
   console.log("pending")
   setTimeout(function(){
    res("resolved")
   },2000)
  })
  var p2=new Promise(function(res,rej){
   res(p1);// resolved状态,但是传递的参数为Promise实例
  })
  p2.then((data)=>{
   console.log(data)
   console.log("在返回的Promise实例p1状态为resolved之后执行")
  })
  /* 
   pending
   // 两秒之后
   resolved
   在返回的Promise实例p1状态为resolved之后执行
   */
  
  // 2. 返回的Promise实例的状态为resolved
  var p3=new Promise(function(res){
   res("p3")
  })
  var p4=new Promise(function(res,rej){
   res(p3)
  })
  p4.then((res)=>{
   console.log(res);//p3,立即执行
  })

6. resolve之后的同步代码执行

  • promise实例改变状态为resolve不会改变同步代码的执行
 // 1. resolved
 new Promise((res,rej)=>{
  res(1);
  console.log("此前状态为resolved,但是依旧会执行本行代码")
 }).then((data)=>{
  console.log(data)
 })
 console.log('宏任务代码')
 /* 
  此前状态为resolved,但是依旧会执行本行代码
  宏任务代码
  1
  */
 
 // 2. rejected
 new Promise((res,rej)=>{
  rej(new Error("错误error:"));
  console.log("此时状态为rejected,本行代码会执行")
 }).then(()=>{},(err)=>{
  console.log(err)
 })
 /* 
  此时状态为rejected,本行代码会执行
  // 然后才会轮到微任务执行
  Error: 错误error:
      at 平时测试.html:130
      at new Promise (<anonymous>)
      at 平时测试.html:129
  */

7.改变状态后即刻结束

  // 如果想要在改变状态为resolved或者rejected之后
  // 不会继续执行往下代码,那么使用return 
  new Promise(function(res,rej){
   return res(1)
   // return rej(1)
   console.log("已经return,所以不会继续执行本行代码")
  }).then((data)=>{
   console.log(data)
  },(err)=>{
   console.log(err)
  })
  // 结果只会打印: 1

三. then()

  • then方法是定义在原型对象Promise.prototype上的,作用是为Promise实例添加状态改变时的回调函数
  • then方法的参数不是函数时会发生穿透。也就是then(1).then((data)=>{}),第二个then获得的参数函数的data就是上次穿透的数据1
  • 如果then方法的参数是函数,那么第一个参数是resolved函数,第二个参数是rejected函数
  • 如果promise实例存在多个then,那么下一个then会等待上一个then返回的promise实例状态改为resolved/rejected才会执行
  // 多个then,需要等待上一个then的状态改为resolved/rejected
  new Promise((res,rej)=>{
   res(1)
  }).then((res)=>{
   console.log(res);
   return new Promise((res,rej)=>{
    setTimeout(res,1000);// 一秒后,状态改为resolve
   })
   
  }).then(()=>{
   console.log(2)
  })
  /* 
  1
  // 1秒后
  2
   */
  • 如果then返回的不是Promise实例对象,那么会发生值穿透
  new Promise((res,rej)=>{
   res(1)
  }).then(2).then((data)=>{
   console.log(data);//1
   return 3;
  }).then((data)=>{
   console.log(data)//3
  })
  /* 
   res(1),函数res传递的是一个number类型的变量
   then(2),不是函数,没有传递值,所以无效,仅仅是一个语句 2
   // 所以1的值会继续穿透,then(2)无效
   1,第二个then属于函数对象,所以能够接收到数据1
   return 3,传递数据3
   最后一个then,打印3,接收到上个then函数返回的数据3
   
   */

四. catch()

1. 基本含义

  • Promise.prototype.catch()方法相等于Promise.prototype.then(null,reject())或者Promise.prototype.then(undefined,reject())
  • 用于指定发生错误时的回调函数。在异步操作抛出错误时,状态变为rejected,就会调用catch方法
  • 另外在then方法指定的回调函数中抛出错误,就会被catch方法捕获
  • 调用完catch方法,状态会变为resolved!
  // 1. 捕获Promise错误
  new Promise(function(res,rej){
   throw new Error("抛出错误")
  }).catch(function(err){
   console.log("err:",err)
  })
  /* 
   err: Error: 抛出错误
       at 平时测试.html:116
       at new Promise (<anonymous>)
       at 平时测试.html:115
   */
  
  // 2. then方法中抛出错误
  new Promise(function(res,rej){
   res(1)
  }).then(function(data){
   throw new Error("then err")
  }).catch(function(err){
   console.log(err)
  })
  /* 
   Error: then err
       at 平时测试.html:131
   */

2. catch()相当于调用reject

  • 在promise的后面使用catch其实相当于在promise内部加上try-catch语句,或者相当于加上一个reject状态改变(在某个条件符合时)
  // 1. catch方法相等于try-catch语句
  new Promise(function(res,rej){
   try{
    throw new Error("try抛出错误")
   }catch(e){
    console.log(e)
   }
  })
  /* 
   Error: try抛出错误
       at 平时测试.html:117
       at new Promise (<anonymous>)
       at 平时测试.html:115
   */
  
  // 2. catch方法还相等于调用reject方法然后在then的第二个参数监听
  new Promise(function(res,rej){
   rej("rej err")
  }).then(()=>{},(err)=>{
   console.log(err)
  });//rej err

3.resolve状态再抛出

  • 由于catch相当于调用rejected状态,并且状态改为rejected后不可以改为resolve状态
  • 所以Promise状态为resolve时,再抛出错误,无法在catch捕获到
  // 1. res(),resolve状态后不可以改为reject状态
  new Promise(function(res,rej){
   res("ok");//状态为resolve
   throw new Error("此时抛出错误无法被捕获到")
  }).catch(function(err){
   console.log(err)
  }).then((data)=>{
   console.log(data);//ok
  })

4. catch与then第二个参数

  • then处于catch之前,则在catch之前就捕获了错误
  new Promise(function(res,rej){
   throw new Error("err")
  }).then(()=>{},(err)=>{
   console.log("reject:",err)
  }).catch(function(data){
   console.log('catch:',data)
  })
  /* 
   reject: Error: err
       at 平时测试.html:116
       at new Promise (<anonymous>)
       at 平时测试.html:115
   */

5. Promise内部抛出的错误不会影响到外部

  // 1. new Promise内部会抛出错误
  new Promise(function(res,rej){
   throw new Error("err")
  })
  // 2.即使上面抛出了错误,剩余的代码依旧会执行
  // 也就是Promise内部抛出的错误不会影响到外部
  setTimeout(()=>{
   console.log("继续执行")
  },1000)
  // 会打印错误,之后打印 "继续执行"

6. try-catch捕获错误

  • try-catch捕获错误分为:try执行之前,try执行过程中,try执行之后
  // 1.语法错误,在执行try代码之前就抛出错误,所以catch无法捕获到
  /* try{
   a.
  }catch(e){
   console.log("myerr:",e)
  } */
  /* 
   Uncaught SyntaxError: Unexpected token '}'
   */
  
  // 2. 在try代码执行过程中抛出的错误(可以捕获到)
  /* function add(x){return x+y};  // 错误函数
  try{
   console.log(add(1))
  }catch(e){
   console.log(e);//ReferenceError: y is not defined
  } */
  
  // 3. 在try代码执行完毕之后抛出的错误(不能捕获)
/*   try{
   setTimeout(function(){
    console.log('定时器')
    // 此时的错误没有被catch捕获到
    a.b;//Uncaught ReferenceError: a is not defined
   },2000)
  }catch(e){
   console.log("捕获到的错误:",e);
  } */
  
  // 4.try/catch无法捕获到Promise异常时因为Promise内部就捕获到异常了,没有往外抛出异常,所以外部的catch无法捕获到异常
  try{
   new Promise(function(res,rej){
    // 当没有在Promise中设置catch时,提示下面语句
    throw new Error("err");//Uncaught SyntaxError: Missing catch or finally after try
   }).then(()=>{},(err)=>{
    console.log("第二个参数:",err)
    /* 
     第二个参数: Error: err
         at 平时测试.html:147
         at new Promise (<anonymous>)
         at 平时测试.html:145
     */
   })
   .catch(function(err){
    console.log("catch:",err)
    /* 没有设置then第二个参数函数时
     catch: Error: err
         at 平时测试.html:147
         at new Promise (<anonymous>)
         at 平时测试.html:145
     */
   })
  }catch(e){
   console.log("promise:",e);// 无法捕获到promise内部错误
  }

7. catch返回的是Promise实例

  • 由于catch返回的是Promise实例,所以catch后面接上的then可以继续执行
  • catch中也可以继续抛出错误,然后被后面的then或者catch继续捕获
  // 1. catch捕获抛出的错误后,then方法可以继续执行
  new Promise(function(){
   throw new Error("err")
  }).catch(function(err){
   console.log(err)
  }).then(function(){
   console.log("catch捕获错误后,还可以继续执行then")
  })
  /* 
   Error: err
       at 平时测试.html:116
       at new Promise (<anonymous>)
       at 平时测试.html:115
    
   catch捕获错误后,还可以继续执行then
   */
  
  // 2. catch中也可以抛出错误,然后被下一个catch捕获
  new Promise(function(res,rej){
   throw new Error("第一个错误")
  }).catch(function(err){
   console.log(err)
   throw new Error("第二个错误")
  }).catch(function(err){
   console.log(err)
  })
  /* 
   Error: 第一个错误
       at 平时测试.html:133
       at new Promise (<anonymous>)
       at 平时测试.html:132
  
  Error: 第二个错误
      at 平时测试.html:136
   */

五. finally

  • ES2018才引入的,就是不管promise的状态是什么,最后都会执行
  • finally方法的回调函数不能接受任何参数,这就意味着不能知道前面的Promise状态到底是fulfilled还是rejected
  • 所以finally方法里面的操作,应该是和状态无关的,不依赖于Promise的执行结果
  • finally方法的本质上就是在promise实例最后加上一个then来接收resolved和rejected两种状态,而加上finally的好处就在于只需要写一个接收函数就行了,不需要知道状态
  • 并且finally方法总是会返回原来的值
  // 1. finally在最后
  new Promise(function(res,rej){
   res(1)
  }).then((data)=>{
   console.log(data);//1
   return 2
  }).finally(()=>{
   console.log(3);//3, finally方法不能接受到参数,所以2接收1不到
  })
  /* 
   1
   3
   */
  
  // 2. finally后面还有then方法
  new Promise((res,rej)=>{
   res(11)
  }).then((data)=>{
   console.log(data);//11
   return 22;
  }).finally(()=>{
   console.log(33);//33
  }).then((data)=>{
   console.log(data);//22,执行上一个被返回的then
  })
  
  // 3. finally后面还有then,并且finally返回值
  new Promise((res,rej)=>{
   res(111)
  }).then((data)=>{
   console.log(data);//111
   return "后续then接收到之前的then,而不会接收到finally return的变量"
  }).finally(()=>{
   console.log(222);//222
   return 333;
  }).then((data)=>{
   console.log(data);//后续then接收到之前的then,而不会接收到finally return的变量
  })

六. Promise.all()

  • Promise.all()方法用于将多个Promise实例包装成一个新的Promise实例。
  • Promise.all()接收一个具有Iterator接口的数据作为参数,关键在于成员必须都是Promise实例
  • 成员都变为resolved之后会返回结果数组;如果有一个成员变为rejected状态,那么返回rejected状态Promise实例的返回值
 // 1. 全部resolved
 var p1=new Promise((res,rej)=>{
  res(1);
 })
 var p2=new Promise((res,rej)=>{
  res(2);
 })
 var p3=new Promise((res,rej)=>{
  res(3);
 })
 // Promise.all()返回的是一个Promise实例
 // 该实例的值存储在内部属性 [[PromiseValue]]
 // 想要获取,需要通过then方法来保存参数
 var res1=Promise.all([p1,p2,p3])
 .then((data)=>{
  console.log(data);//[1, 2, 3]
  return data;
 })
 console.log(res1);//Promise {<pending>}
 
 // 2. 有一个rejected,其他的停止执行
 var p4=new Promise((res,rej)=>{
  console.log("p4")
  setTimeout(res(4),5000);
 })
 // 2.1 错误没被捕获
 var p5=new Promise((res,rej)=>{
  setTimeout(rej("error:"+5555),1000)
 })
 
 var p6=new Promise((res,rej)=>{
  console.log("p6")
  res(6)
 })
 /* var res2=Promise.all([p4,p5,p6]).then((data)=>{
  console.log("all:",data);
 }) */
 
 // 2.2 
 /* var p5=new Promise((res,rej)=>{
  setTimeout(rej("error:"+5555),1000)
 }).catch(function(err){
  console.log("err",err);//err error:5555
 }).then((res)=>{
  return "res"
 })
 console.log(p5) */
 
 // 2.3 给Promise.all后面添加catch捕获错误
 var res2=Promise.all([p4,p5,p6]).then((data)=>{
  console.log("all:",data);
 }).catch((err)=>{
  console.log("all+err",err);//all+err error:5555
 })
 /* 
  2.1 没有给p5设置catch捕获错误,所以最后执行错误,Promise.all没接住错误
  2.2 给p5设置catch捕获错误,则Promise.all剩余实例也会执行,但是返回rej的实例的值可能是undefined
  也就是Promise.all的结果是[4,undefined,6]
  2.3 给Promise.all后面添加catch捕获错误,打印all+err error:5555
  2.4 如果实例和Promise.all都设置了catch,那么使用实例的catch来捕获错误,因为实例离得近
  */

多个任务异常

  • 使用promise.all时存在多个异步任务异常,catch只捕获最早的那个错误
   function runAsync (x) {
     const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
     return p
   }
   function runReject (x) {
     const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
     return p
   }
   Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
     .then(res =>console.log(res))
     .then(res =>console.log(res),rej=> console.log('error:',rej))
     .catch(err =>console.log(err))
  • 多个异步任务是并行执行的,哪个先执行完毕不一定
  • 当all方法出现异常时,all之后的then接收的是reject函数,参数就是第一个异常返回的参数
  • 当all方法之后的then不存在reject函数时,则catch接受异常参数
  • 即使不是all之后的第一个then存在reject函数,只要之后的then存在reject函数,就不会轮到catch去接收异常
  • 如果异步任务中存在报错,那么res不会执行,rej才会执行,也就是报错
  • 如果不存在报错,那么就会执行res

七. promise.race()

  • race方法也是接收一组异步任务,然后并行执行异步任务,但是只保留第一个执行结果,剩下的异步任务仍在执行,但是执行结果无法获取
  • promise.race虽然只会接收第一个任务的结果,但是其他异步任务依旧会执行
 function runAsync(x) {
   const p = new Promise(r =>
     setTimeout(() => r(x, console.log(x)), 1000)
   );
   return p;
 }
 function runReject(x) {
   const p = new Promise((res, rej) =>
     setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
   );
   return p;
 }
 /* 
  首先打印0,然后返回rej('Error:0')
  该rej被Promise.race的catch捕获,但是剩下的成员依旧执行
  所以runAsync还会打印 1 2 3
  */
 Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])
   .then(res =>console.log("result: ", res))
   .catch(err =>console.log(err));// Error:0

八. Promise.resolve()

  • 将现有的对象转换为resolve状态的异步对象就使用Promise.resolve()
 // 1. 使用Promise.resolve()
 var res1=Promise.resolve("1");
 console.log(res1);//Promise {<resolved>: "1"}
 
 // 2. 等同于 new Promise((res,rej)=> res())
 var res2=new Promise((res,rej)=>{
  res("2")
 })
 console.log(res2);//Promise {<resolved>: "2"}

四种参数情况

  1. 参数为Promise实例,那么就相当于Promise.resolve啥都没做,无效
  • 原封不动的返回原来的Promise实例
  1. 参数是一个thenable对象
  • thenable对象指的是具有then方法的对象
  • Promise.resolve方法会把thenable对象的then方法执行,然后resolve方法的函数参数就是then中传递的参数
  • 注意thenable对象需要有参数res/rej,然后执行res(xxx)用于传递数据
 var a={
  then:function(res){
   res(444)
  }
 }
 Promise.resolve(a).then((data)=>{
  console.log(data);//444
 })
  1. 参数是没有then方法的对象或者不是对象
  • 此时直接传递该参数
 // 1. 参数不是对象
 Promise.resolve('hello world').then((data)=>{
  console.log(data);//hello world
 })
 
 // 2. 参数是对象,但是没有then方法
 Promise.resolve({a:'hello'}).then((data)=>{
  console.log(data);//{a: "hello"}
 })
  1. 不带参数
  • 不带参数就then接收不到数据,还是可以使用!

九. Promise.reject()

  • Promise.reject()方法会返回一个新的Promise实例,实例状态为rejected
  • 与Promise.resolve()不同之处在于,reject无论参数为什么,都是直接返回该参数!
  • 注意要使用catch接收,否则会报错,因为是reject状态
 // 1. thenable对象,直接返回参数
 var obj={
  then:function(res,rej){
   rej("err")
  }
 }
 Promise.reject(obj).catch((e)=>{
  console.log(e);//{then: ƒ}
 })
 
 //2. 其他参数
 Promise.reject("he").catch((res)=>{
  console.log(res);// he
 })

十. 应用

1. 手写promise

   function mypromise(func){
    var that=this;
    // 回调函数集
    that.funclist=[];
    // resolve方法
    function resolve(value){
     // 微任务
     setTimeout(()=>{
      that.data=value
      that.funclist.forEach((callback)=>{
       callback(value)
      })
     })
    }
    // 执行用户传入的函数
    func(resolve.bind(that))
   }
   mypromise.prototype.then=function(onResolved){
    var that=this;
    return new mypromise(resolve => {
     that.funclist.push(function(){
      var res=onResolved(that.data);
      if(res instanceof mypromise){
       res.then(resolve)
      }else{
       resolve(res);
      }
     })
    })
   }
   const func=resolve => {
    setTimeout(()=>{
     resolve(1)
    },1000)
   }
   var promise1 = new mypromise(func)
   promise1.then(res => {
     console.log(res)
     return new mypromise(resolve => {
       setTimeout(() => {
         resolve(2)
       }, 500)
     })
   })
   console.log(promise1);

2. 预加载图片

 // 用于预加载图片
 function loadImg(url){
  return new Promise(function(res,rej){
   var img=document.createElement('img');
   img.src=url;  // 设置了src,也就是默认开始请求图片资源了
   img.onload=res;//指向函数res 
   img.onerror=rej;
  })
 }
 console.log(loadImg("http://yiyeblog.com/imgs/ES6.png"))
  • 可以在NetWork中看到成功加载图片

本文使用 mdnice 排版