浅谈promise(包含async与await)

169 阅读9分钟
  • 声明:本文用于笔者前端学习阶段的知识点整理,如有遗漏与问题,请多指教,部分内容源自互联网,侵删。

1.基础知识

1.1 同步与异步代码

  • 在了解什么是promise之前,需要先了解什么是同步代码和异步代码
  • 这里简单的举一个例子:
    • 假如你去吃饭,要点单服务员却告诉你,你必须等待他服务完上一桌,才能给你服务,显然这体验极差.正常情况下,是你决定要点单了,会有另外一个服务员出来,给你服务,这是比较正常的情况.
    • 当你点完单,服务员告诉你,现在大厨正在给上一桌客人做饭,你需要等大厨做完上一桌才能给你做,你才能吃上饭
  • 这里映射回代码中
    • 假如现在有一个定时器,设置1w秒后执行,按照代码从上到下执行的顺序,如果要等这1w秒之后再执行后面的代码,显然是不现实的
    • 所以我们需要将这个定时器,拿到一个位置,等待某个时间点执行其中的代码
  • 上述这样的情况就产生同步代码和异步代码的概念

    • 同步代码: 必须等到前面的同步代码执行完毕,得到结果才能执行下段同步代码.
    • 异步代码: 不等待任务执行完毕,就执行其后面的代码.

1.2 异步代码的执行机制

  • 浏览器中有JS引擎来解析JS代码,JS引擎,会进行预解析
  • 1.JS引擎预解析的过程中,会将同步代码移动到主线程上排队执行
    • 主线程:JS中只有唯一的且为单向的线程,所有的代码都必须在这个线程上执行
  • 2.会将异步代码,移动到宿主环境中
    • 宿主环境:由外壳程序创建与维护,只要能提供js引擎执行的环境都可称之为外壳程序,例如:web浏览器等就是宿主环境
  • 3.预解析其在解析异步代码时,会将异步代码分为宏任务与微任务
    • ES6 规范中,microtask 称为 jobsmacrotask 称为 task
      宏任务是由宿主发起的,而微任务由JavaScript自身发起.
    • ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。

  • 微任务有(不一一列举):
    • process.nextTick只有nodejs
    • promise.then().catch().finally() 成功失败都会触发finally()中的回调,pending是不会触发的。
  • 宏任务有(不一一列举):
    • setTimeout 定时器
    • setInterval延时函数
    • script标签
  • 4.event loop(事件循环)

  • 先上图,方便理解

  • 它就是异步代码的执行机制,也就是等待宿主环境分配宏任务 反复等待 执行的过程就是事件循环。
  • Event Loop中,每一次循环称为tick,每一次tick的任务如下:
    • 执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
    • 检查是否存在微任务,有则会执行至微任务队列为空;
    • 如果宿主为浏览器,可能会渲染页面;
    • 开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调).

1.3 抛出问题

  • 这个时候就出现了一个问题,我们依旧是用一个现实的例子来引出
    • 依旧去饭店去吃饭,我们点完菜之后,因为点的是一个需要挺长时间才能做好的菜,但是主食是提前做好的,所以先主食给先上了,但是我们需要有菜才能吃,这样对消费者的体验就不好
  • 映照回代码;
    • 假设现在页面中进行了两次ajax请求,我们需要通过第一次ajax请求回来的数据发起第二次请求,但是由于第一次请求的数据很多,所以很慢,第二次请求没有等到第一次请求完成就发送了请求,这个时候就会出现明显的问题,第二次请求没有数据,就会请求出错
  • 解决方法;
    • 解决这种问题的方法也很简单,就是等到第一个请求执行完毕,再执行第二个请求,就是使用一种嵌套的形式
  • 缺点:
    • 通过这种回调形式是可以解决异步代码执行中的不确定性的问题,但是随之带来的是复杂的嵌套格式,在代码较多的情况下,甚至会形成回调地狱
  • 正当程序员们在苦恼回调地狱这个问题时,promise就诞生了

2.promise的使用

  • 在明确使用方法前,先介绍一下promise的三种状态
  • promise的状态不受外界影响 (3种状态)
    • Pending状态(进行中)
    • Fulfilled状态(已成功)
    • Rejected状态(已失败)
  • 1.创建Promise实例
    • Promise构造函数接受一个回调函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由JavaScript引擎提供,不用自己部署。
      • resolve是将promise对象的状态从Pending状态,转换为Fulfilled状态,异步操作成功时,将结果作为参数传递出
      • reject是将promise对象的状态从padding状态转换为Rejected状态,异步操作失败时,将结果传递出去
  • 2.then方法
    • 在promise实例生效后,可用then方法分别指定两种状态回调参数.then 方法可以接受两个回调函数作为参数:
      • Promise对象状态改为Resolved时调用 (必选)
      • Promise对象状态改为Rejected时调用 (可选)
// 基本用法

function sleep(ms) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, ms);
    })
}
sleep(500).then( ()=> console.log("finished")); //500毫秒后执行.then中的参数
  • 3.then方法中的return(重点)
    • 之所以promise可以解决回调地狱的问题,就在于.then方法中的return
    • .then方法return另一个promise实例对象,就可以继续调用.then方法,而且我们刚才说明了.then方法中的结果都是代码执行结束的结果,这样就保证了上一个异步任务执行完毕,再执行下一个异步任务
  • 4.catch方法
    • 如果不在.then中获取失败的信息,可以在.catch方法中获取
    • 优势在于,可以获取所有promise实例中的错误信息
  • 5.对于.then方法的执行顺序,这里贴一个实例
let promise = new Promise(function(resolve, reject){
    console.log("1");
    resolve();
});
setTimeout(()=>console.log("2"), 0);
promise.then(() => console.log("3"));
console.log("4");

// 1
// 4
// 3
// 2
  • 代码在执行时 1 与 4是同步代码,执行顺序都懂,问题在于 2 与 3 的执行顺序
    • 定时器是另一个宏任务,需要在任务队列等待执行
    • promise.then是微任务,会在同步代码执行结束之后,自动执行

3.async 与 await

3.1 引出async与await

  • 在解释async与await之前,先来体会一个案例
// 现在页面中有四次axios请求,要按照顺序调用
new Promise(function(resolve){
    ajaxA("xxxx", ()=> { resolve(); })    
}).then(function(){
    return new Promise(function(resolve){
        ajaxB("xxxx", ()=> { resolve(); })    
    })
}).then(function(){
    return new Promise(function(resolve){
        ajaxC("xxxx", ()=> { resolve(); })    
    })
}).then(function(){
    ajaxD("xxxx");
});  
  • 上述的语法看起来是不是比较混乱,如果我们将axios请求封装成函数,代码看起来会简洁不少
// 将请求封装成一个对象
function request() {
	// 省略代码....
  return new Promise(...) // 需要返回一个Promise对象
}
// 代码改造
request("ajaxA")
.then((data)=>{
   //处理data
   return request("ajaxB")
})
.then((data)=>{
   //处理data
   return request("ajaxC")
})
.then((data)=>{
   //处理data
   return request("ajaxD")
})
  • 代码简洁了许多,但是不够简洁,在这里我们先体验一下用async与await修改代码样式
async function load(){
    await request("ajaxA");
    await request("ajaxB");
    await request("ajaxC");
    await request("ajaxD");
}
  • 现在这段代码是不是看起来就像同步代码一样,代码的阅读和编写都变得简单了,更重要的是代码的执行顺序也很清晰
  • 所以,一句话就可以解释async和await的作用, 解决then方法的反复调用,编写冗余的问题

3.2 使用async与await

3.2.1async修饰函数

  • async修饰的函数返回值的是Promise对象,当async函数没有返回值时,返回值是Promise.resolve(undefined)
// 有返回值
async function hello() {
    return "hello async";
}

const res = hello();
console.log(res); // Promise {<fulfilled>: "hello async"}

// 无返回值
async function hello() {
   "hello async";
}

const res = hello();
console.log(res); // Promise {<fulfilled>: undefined}
  • 因为其返回的是一个Promise对象所以可以调用then方法
hello().then(res => {
    console.log(res);    // hello async
});

3.2.2await使用

  • 1.await只能放在async修饰的函数内部使用,否则就会报错
  • 2.await如果等到不是一个Promise对象,就返回它等到的东西,如果等到的是一个Promise对象,那await就会阻碍其后代码的执行,等待Promise的resolve的值,作为其返回值
// 看一看下面代码的执行顺序
console.log(1)
async function hello() {
  console.log(2)
  await setTimeout(()=>console.log(4),0)
  console.log(3)
}

hello();
// 1 2 4 3
//由此可见 await 接收到promise对象后会阻碍后面的代码执行

3.2.3错误处理

  • 在async修饰的函数中,无论是Promise reject的数据错误还是逻辑错误都无法显示,会被默默吞掉,所以想要得到错误最好的方式,就是去捕捉错误,可以用try{},catch{}的方式捕捉Promise对象中的reject中的错误
function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {reject('error')}, ms);  //reject模拟出错,返回error
  });
}

async function res(ms) {
  try {
     console.log('输出了正确的内容');

     await timeout(ms);  //这里返回了错误

     console.log('输出了错误的内容');  //所以这句代码不会被执行了

  } catch(err) {
     console.dir(err); //这里捕捉到错误error,并打印在工具台
  }
}
res(1000);

3.2.4注意事项

  • async和await属于ES8的新语法,兼容性很差,但是在我们自己做项目的时候可以借助babel来进行降级
  • 安装依赖:
npm i babel-preset-es2015 babel-preset-stage-3 babel-runtime babel-plugin-transform-runtime
  • 修改配置
 "presets": ["es2015", "stage-3"],

 "plugins": ["transform-runtime"]

这样就可以在项目中使用 async 函数了。