Promise用法小结

403 阅读8分钟

前言

看着自己的简历,感觉像个实践派。看上去做了好多好多项目,自己确真的不了解Promise。每次面试官问道,我感觉都是胡乱答一通。为此我决定好好的去思考这个问题,结合各路大神的文章,我自己好好的搬运一回。

一、Promise 的出现?

话不多说,我们先看一个例子

请求1((请求结果1)=>{
    处理结果1
})

但是如果是这样呢

请求1((请求结果1)=>{
    请求2((请求结果2)=>{
        请求3((请求结果3)=>{
        ......
    })
    })
})

实际的一个例子如下,一个内嵌H5活动页。
一进页面,--->先获取app端回调的用户信息,拿到uuid,-->马上去请求商品列表-->再去点进去后买。

getUserInfo->getGoodsList->pay

getUserInfo((res)=>{
    getGoodsList((res.uuid,(good))=>{
        pay((goods.uuid,res.uuid)=>{
        ......
    })
    })
})

实际上业界俗称的,地狱回调就出现了。那么解决问题就有了Promise。

二、请简单说一下Promise是什么?

简单来说就是一个容器,里面保存着某个未来才会结束的事件的结果。从语法层面来说,Promise实际是个对象,从它可以获取异步操作的消息。

再提炼来说,实际上就是解决异步变成的一种方案。

那么对于上述的问题,解决方案就有了

new Promise((请求1){
    
}).then((请求2){
    请求结果1
}).then((请求3){
    请求结果2
}.then((请求4){
    请求结果3
}).cath((处理异常){
    捕捉异常
})

三、基本用法

3.1、官方给出的基本用法如下:

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

1、Promise构造函数接受一个函数(executor)作为参数,这个函数的参数有两个,resolve(未完成-->成功(resolved))、reject(未完成-->失败(rejected))
2、Promise构造函数执行-->立即调用executor函数-->resolve,reject两个函数作为参数传递给executor函数
3、调用resolve函数后,一切正常(resolved),promise.then
4、调用reject函数后,抛出异常(rejected)promise.catch

平时使用Promise基本结构

const a = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log("你好");
  }, 1000);
  console.log("你好1");
});

a.then((res)=>{
    //some code
})

a.catch((err)=>{
    //some code
})

a.finally(()=>{
    
})

接下来就看个简单的例子:

const a = new Promise((resolve, reject) => {

  setTimeout(() => {
    console.log("你好");
  }, 1000);
  console.log("你好1");
   /* executor */
});

console.log("你好2"); // 你好1   你好2   你好

Promise -->回调延迟绑定,接着👇代码

let p1 = new Promise((resolve, reject) => {
  console.log(1);
  resolve("测试");
  console.log(2);
});

p1.then(
  result => {
    console.log("成功 " + result);
  },
  reason => {
    console.log("失败 " + reason);
  }
);
console.log(3);  
// 1 2 3 成功 测试

Promise新建后立即执行,所以首先输出的是1、2 ,Promise在执行resolve时,触发微任务,将在当前脚本所有同步任务执行完才会执行,p1.then,存储起来两个函数,所以接下来输出的是3,再接下来就是then方法里面成功回调,输出成功,最后就是执行resolve(),测试

3.2、错误处理:

  return new Promise((resolve, reject) => {
  apiPromise   //封装接口请求
    .then((resp) => {
        //成功逻辑
      } else {
        // 失败逻辑
      }
    }).then(()=>{
        //todo
    })
    .catch(reject){
      //捕获异常操作
    };
});

不管是多层嵌套调用then方法,都可以通过最后一个对象.catch() 来捕获异常。

3.3、(重要)接着上面继续梳理,Promise链式调用,参考简书一博主的一个例子,如下

new Promise((resolve, reject) => {
  console.log("log: 外部promise");
  resolve();
})
  .then(() => {
    console.log("log: 外部第一个then");
    new Promise((resolve, reject) => {
      console.log("log: 内部promise");
      resolve();
    })
      .then(() => {
        console.log("log: 内部第一个then");
      })
      .then(() => {
        console.log("log: 内部第二个then");
      });
  })
  .then(() => {
    console.log("log: 外部第二个then");
  });

// log: 外部promise
// log: 外部第一个then
// log: 内部promise
// log: 内部第一个then
// log: 外部第二个then
// log: 内部第二个then

对于运行后的结果,我还是比较懵逼的。

参照资料慢慢梳理:

1、最先输出的log:外部promise,是因为每当new一个Promise实例的时候,除了会得到一个promise实例之外,会立即执行executor函数,所以就会马上输出。
2、接着resolve()被调用,然后就会执行外部的第一个then(),所以就会输出log:外部第一个then。与此同时外部的第二then(),将他们依次放入微任务队列中。
3、再接下来,又创建了new一个Promise实例,里面的运行机制上面1、2所述,所以就会输出log:内容promise、log: 内部第一个then
4、最后在按顺序一次执行被挂起的微任务队列中的任务log: 外部第二个thenlog: 内部第二个then

总结

执行then方法时,如果前面的promise已经是resolved,则直接将回调放入微任务队列中

new Promise((resolve, reject) => {
  resolve("测试");
})
.then(() => {
    console.log("log: 外部第一个then");
})
.then(() => {
    console.log("log: 外部第二个then");
});

当一个 promise 被 resolve 时,会遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中

四、Promise常用方法

4.1、Promise.all()\Promise.race()

Promise.all、Prmise.race 方法将多个Promise实例,包装成一个新的Promise实例

const p = Promise.all([p1, p2, p3]);

Promise.all--> 上面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1、p2、p3决定,分成两种情况。

(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

const p = Promise.race([p1, p2, p3]);

Promise.race--->p1,p2,p3,之中有一个实例 率先改变状态,p的状态就跟着改变。

五、Promise 宏任务\微任务\事件轮询(Event-Loop)

5.1、宏任务、微任务都表示异步任务的两种分类。例如,经常在面试题、各种大咖博文里的代码片段:

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
})

console.log(2)


// 1 2 3 4

上文中,setTimeout,就是用来储存宏任务的,实例化new Promise,执行的代码片段都是同步进行的,而then中注册的回调是异步执行的。也就是说同步代码执行过后,才会去检查是否有异步任务完成,并执行相应的回调。注意微任务的执行是在宏任务之前。

5.2、常见宏任务、微任务分类:

  • 常见宏任务:setTimeout、setInterval、setImmediate....
  • 常见微任务:Promise.then catch finally、setImmediate.....

5.3、事件轮询(Event-Loop):

  • 什么是Event-Loop,JS是一个单进程的语言,同一时间不能处理多个任务,所以何时执行宏任务,何时执行微任务,就要有一个判断逻辑的存在。也就是说,宏任务队列--->挂起--->微任务1、微任务2、......--->顺序执行 。

六、宏任务、微任务常见的的几种情况分析

6.1、主线程中创建宏任务和微任务,感觉上述的例子依旧还能说明

setTimeout(_ => console.log(4))   // 将回调代码放入挂机放到另一个宏任务队列

new Promise(resolve => {
  console.log(1)
  resolve()
}).then(_ => {
  console.log(3)  // 将回调代码放入微任务队列
})

console.log(2)

// 1 2 3 4

执行任务顺序:主线程-->主线程上创建的微任务-->主线程上创建的宏任务

6.2、微任务中创建微任务

setTimeout(_ => console.log(4));

new Promise(resolve => {
  resolve();
  console.log(1);
}).then(_ => {
  console.log(3);
    Promise.resolve()
    .then(_ => {
      console.log("第一次");
    })
    .then(_ => {
      Promise.resolve().then(_ => {
        console.log("第二次");
      });
    });
});

console.log(2);

//1 2 3 第一次 第二次 4

执行任务顺序:上述例子上来说,依旧还是先执行:主线程-->主线程创建的微任务1-->微任务2-->....主线程创建的宏任务

6.3、宏任务中创建微任务

setTimeout(() => {
  console.log("最外层宏任务1");
  setTimeout(() => {
    console.log("最外层宏任务2");
  }, 0);
  new Promise(resolve => {
    resolve();
    console.log(1);
  }).then(_ => {
    console.log(3);
    Promise.resolve()
      .then(_ => {
        console.log("第一次");
      })
      .then(_ => {
        Promise.resolve().then(_ => {
          console.log("第二次");
        });
      });
  });
}, 0);

console.log(2);

//2 最外层宏任务1 1 3 第一次 第二次 最外层宏任务2

定论

  • 1、微任务队列优于宏任务队列执行。
  • 2、微任务队列上创建的宏任务会被后添加的当前宏任务队列的尾端。
  • 3、微任务队列上创建的微任务会被添加到微任务队列的尾端。
  • 4、只要微任务队列中还有任务,宏任务队列就会等待微任务队列执行完毕后在执行。

七、关于Promise的问题汇总

1、了解Promise吗?
2、Promise 如何使用?
3、Promise 常用的方法有哪些?它们的作用是什么?
4、Promie出现的原因?

参考文章

面试精选之Promise
你真的懂Promise吗
Promise 链式调用顺序引发的思考
这一次,彻底弄懂 JavaScript 执行机制