前言

在异步编程中,Promise 表演了无足轻重的角色,比传统的解决方案(回调函数和事件)更正当和更弱小。有些敌人对于这个简直每天都在打交道的“老朋友”,貌似全懂,但稍加深刻就可能疑难百出,本文带大家深刻了解这个相熟的陌生人—— Promise.

根本用法

1.语法

new Promise( function(resolve, reject) {...} /* executor */ )

  • 构建 Promise 对象时,须要传入一个 executor 函数,次要业务流程都在 executor 函数中执行。
  • Promise构造函数执行时立刻调用executor 函数, resolve 和 reject 两个函数作为参数传递给executor,resolve 和 reject 函数被调用时,别离将promise的状态改为fulfilled(实现)或rejected(失败)。一旦状态扭转,就不会再变,任何时候都能够失去这个后果。
  • 在 executor 函数中调用 resolve 函数后,会触发 promise.then 设置的回调函数;而调用 reject 函数后,会触发 promise.catch 设置的回调函数。

值得注意的是,Promise 是用来治理异步编程的,它自身不是异步的,new Promise的时候会立刻把executor函数执行,只不过咱们个别会在executor函数中解决一个异步操作。比方上面代码中,一开始是会先打印出2。

let p1 = new Promise(()=>{    setTimeout(()=>{      console.log(1)    },1000)    console.log(2)  })console.log(3) // 2 3 1

Promise 采纳了回调函数提早绑定技术,在执行 resolve 函数的时候,回调函数还没有绑定,那么只能推延回调函数的执行。这具体是啥意思呢?咱们先来看上面的例子:

let p1 = new Promise((resolve,reject)=>{  console.log(1);  resolve('JAVA_朴学生')  console.log(2)})// then:设置胜利或者失败后处理的办法p1.then(result=>{ //p1提早绑定回调函数  console.log('胜利 '+result)},reason=>{  console.log('失败 '+reason)})console.log(3)// 1// 2// 3// 胜利 JAVA_朴学生

new Promise的时候先执行executor函数,打印出 1、2,Promise在执行resolve时,触发微工作,还是持续往下执行同步工作,
执行p1.then时,存储起来两个函数(此时这两个函数还没有执行),而后打印出3,此时同步工作执行实现,最初执行刚刚那个微工作,从而执行.then中胜利的办法。

错误处理

Promise 对象的谬误具备“冒泡”性质,会始终向后传递,直到被 onReject 函数解决或 catch 语句捕捉为止。具备了这样“冒泡”的个性后,就不须要在每个 Promise 对象中独自捕捉异样了。

要遇到一个then,要执行胜利或者失败的办法,但如果此办法并没有在以后then中被定义,则顺延到下一个对应的函数

function executor (resolve, reject) {  let rand = Math.random()  console.log(1)  console.log(rand)  if (rand > 0.5) {    resolve()  } else {    reject()  }}var p0 = new Promise(executor)var p1 = p0.then((value) => {  console.log('succeed-1')  return new Promise(executor)})var p2 = p1.then((value) => {  console.log('succeed-2')  return new Promise(executor)})p2.catch((error) => {  console.log('error', error)})console.log(2)

这段代码有三个 Promise 对象:p0~p2。无论哪个对象外面抛出异样,都能够通过最初一个对象 p2.catch 来捕捉异样,通过这种形式能够将所有 Promise 对象的谬误合并到一个函数来解决,这样就解决了每个工作都须要独自解决异样的问题。

通过这种形式,咱们就毁灭了嵌套调用和频繁的错误处理,这样使得咱们写进去的代码更加优雅,更加合乎人的线性思维。

Promise链式调用

咱们都晓得能够把多个Promise连贯到一起来示意一系列异步骤。这种形式能够实现的关键在于以下两个Promise 固有行为个性:

  • 每次你对Promise调用then,它都会创立并返回一个新的Promise,咱们能够将其链接起来;
  • 不论从then调用的实现回调(第一个参数)返回的值是什么,它都会被主动设置为被链接Promise(第一点中的)的实现。

先通过上面的例子,来解释一下刚刚这段话是什么意思,而后具体介绍下链式调用的执行流程

let p1=new Promise((resolve,reject)=>{    resolve(100) // 决定了下个then中胜利办法会被执行})// 连贯p1let p2=p1.then(result=>{    console.log('胜利1 '+result)    return Promise.reject(1) // 返回一个新的Promise实例,决定了以后实例是失败的,所以决定下一个then中失败办法会被执行},reason=>{    console.log('失败1 '+reason)    return 200})// 连贯p2 let p3=p2.then(result=>{    console.log('胜利2 '+result)},reason=>{    console.log('失败2 '+reason)})// 胜利1 100// 失败2 1

咱们通过返回 Promise.reject(1) ,实现了第一个调用then创立并返回的promise p2。p2的then调用在运行时会从return Promise.reject(1) 语句承受实现值。当然,p2.then又创立了另一个新的promise,能够用变量p3存储。

new Promise进去的实例,胜利或者失败,取决于executor函数执行的时候,执行的是resolve还是reject决定的,或executor函数执行产生异样谬误,这两种状况都会把实例状态改为失败的。

p2执行then返回的新实例的状态,决定下一个then中哪一个办法会被执行,有以下几种状况:

  • 不论是胜利的办法执行,还是失败的办法执行(then中的两个办法),但凡执行抛出了异样,则都会把实例的状态改为失败。
  • 办法中如果返回一个新的Promise实例(比方上例中的Promise.reject(1)),返回这个实例的后果是胜利还是失败,也决定了以后实例是胜利还是失败。
  • 剩下的状况基本上都是让实例变为胜利的状态,上一个then中办法返回的后果会传递到下一个then的办法中。

咱们再来看个例子

new Promise(resolve=>{    resolve(a) // 报错 // 这个executor函数执行产生异样谬误,决定下个then失败办法会被执行}).then(result=>{    console.log(`胜利:${result}`)    return result*10},reason=>{    console.log(`失败:${reason}`)// 执行这句时候,没有产生异样或者返回一个失败的Promise实例,所以下个then胜利办法会被执行// 这里没有return,最初会返回 undefined}).then(result=>{    console.log(`胜利:${result}`)},reason=>{    console.log(`失败:${reason}`)})// 失败:ReferenceError: a is not defined// 胜利:undefined

async & await

从下面一些例子,咱们能够看出,尽管应用 Promise 能很好地解决回调天堂的问题,然而这种形式充斥了 Promise 的 then() 办法,如果解决流程比较复杂的话,那么整段代码将充斥着 then,语义化不显著,代码不能很好地示意执行流程。

ES7中新增的异步编程办法,async/await的实现是基于 Promise的,简略而言就是async 函数就是返回Promise对象,是generator的语法糖。很多人认为async/await是异步操作的终极解决方案:

  • 语法简洁,更像是同步代码,也更合乎一般的浏览习惯;
  • 改良JS中异步操作串行执行的代码组织形式,缩小callback的嵌套;
  • Promise中不能自定义应用try/catch进行谬误捕捉,然而在Async/await中能够像解决同步代码处理错误。

不过也存在一些毛病,因为 await 将异步代码革新成了同步代码,如果多个异步代码没有依赖性却应用了 await 会导致性能上的升高。

async function test() {  // 以下代码没有依赖性的话,齐全能够应用 Promise.all 的形式  // 如果有依赖性的话,其实就是解决回调天堂的例子了  await fetch(url1)  await fetch(url2)  await fetch(url3)}

察看上面这段代码,你能判断出打印进去的内容是什么吗?

let p1 = Promise.resolve(1)let p2 = new Promise(resolve => {  setTimeout(() => {    resolve(2)  }, 1000)})async function fn() {  console.log(1)// 当代码执行到此行(先把此行),构建一个异步的微工作// 期待promise返回后果,并且await上面的代码也都被列到工作队列中  let result1 = await p2  console.log(3)  let result2 = await p1  console.log(4)}fn()console.log(2)// 1 2 3 4

如果 await 右侧表白逻辑是个 promise,await会期待这个promise的返回后果,只有返回的状态是resolved状况,才会把后果返回,如果promise是失败状态,则await不会接管其返回后果,await上面的代码也不会在继续执行。

let p1 = Promise.reject(100)async function fn1() {  let result = await p1  console.log(1) //这行代码不会执行}

咱们再来看道比较复杂的题目:

console.log(1)setTimeout(()=>{console.log(2)},1000)async function fn(){    console.log(3)    setTimeout(()=>{console.log(4)},20)    return Promise.reject()}async function run(){    console.log(5)    await fn()    console.log(6)}run()//须要执行150ms左右for(let i=0;i<90000000;i++){}setTimeout(()=>{    console.log(7)    new Promise(resolve=>{        console.log(8)        resolve()    }).then(()=>{console.log(9)})},0)console.log(10)// 1 5 3 10 4 7 8 9 2 

做这道题之前,读者需明确:

  • 基于微工作的技术有 MutationObserver、Promise 以及以 Promise 为根底开发进去的很多其余的技术,本题中resolve()、await fn()都是微工作。
  • 不论宏工作是否达到工夫,以及搁置的先后顺序,每次主线程执行栈为空的时候,引擎会优先解决微工作队列,解决完微工作队列里的所有工作,再去解决宏工作。

接下来,咱们一步一步剖析:

  • 首先执行同步代码,输入 1,遇见第一个setTimeout,将其回调放入工作队列(宏工作)当中,持续往下执行
  • 运行run(),打印出 5,并往下执行,遇见 await fn(),将其放入工作队列(微工作)
  • await fn() 以后这一行代码执行时,fn函数会立刻执行的,打印出3,遇见第二个setTimeout,将其回调放入工作队列(宏工作),await fn() 上面的代码须要期待返回Promise胜利状态才会执行,所以6是不会被打印的。
  • 持续往下执行,遇到for循环同步代码,须要等150ms,尽管第二个setTimeout曾经达到工夫,但不会执行,遇见第三个setTimeout,将其回调放入工作队列(宏工作),而后打印出10。值得注意的是,这个定时器 推迟时间0毫秒实际上达不到的。依据HTML5规范,setTimeOut推延执行的工夫,起码是4毫秒。
  • 同步代码执行结束,此时没有微工作,就去执行宏工作,下面提到曾经到点的setTimeout先执行,打印出4
  • 而后执行下一个setTimeout的宏工作,所以先打印出7,new Promise的时候会立刻把executor函数执行,打印出8,而后在执行resolve时,触发微工作,于是打印出9
  • 最初执行第一个setTimeout的宏工作,打印出2

罕用的办法

1、Promise.resolve()

Promise.resolve(value)办法返回一个以给定值解析后的Promise 对象。
Promise.resolve()等价于上面的写法:

Promise.resolve('foo')// 等价于new Promise(resolve => resolve('foo'))

Promise.resolve办法的参数分成四种状况。

(1)参数是一个 Promise 实例

如果参数是 Promise 实例,那么Promise.resolve将不做任何批改、一成不变地返回这个实例。

const p1 = new Promise(function (resolve, reject) {  setTimeout(() => reject(new Error('fail')), 3000)})const p2 = new Promise(function (resolve, reject) {  setTimeout(() => resolve(p1), 1000)})p2  .then(result => console.log(result))  .catch(error => console.log(error))// Error: fail

下面代码中,p1是一个 Promise,3 秒之后变为rejected。p2的状态在 1 秒之后扭转,resolve办法返回的是p1。因为p2返回的是另一个 Promise,导致p2本人的状态有效了,由p1的状态决定p2的状态。所以,前面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch办法指定的回调函数。

(2)参数不是具备then办法的对象,或基本就不是对象

Promise.resolve("Success").then(function(value) { // Promise.resolve办法的参数,会同时传给回调函数。  console.log(value); // "Success"}, function(value) {  // 不会被调用});

(3)不带有任何参数

Promise.resolve()办法容许调用时不带参数,间接返回一个resolved状态的 Promise 对象。如果心愿失去一个 Promise 对象,比拟不便的办法就是间接调用Promise.resolve()办法。

Promise.resolve().then(function () {  console.log('two');});console.log('one');// one two

(4)参数是一个thenable对象

thenable对象指的是具备then办法的对象,Promise.resolve办法会将这个对象转为 Promise 对象,而后就立刻执行thenable对象的then办法。

let thenable = {  then: function(resolve, reject) {    resolve(42);  }};let p1 = Promise.resolve(thenable);p1.then(function(value) {  console.log(value);  // 42});

2、Promise.reject()

Promise.reject()办法返回一个带有回绝起因的Promise对象。

new Promise((resolve,reject) => {    reject(new Error("出错了"));});// 等价于 Promise.reject(new Error("出错了"));  // 应用办法Promise.reject(new Error("BOOM!")).catch(error => {    console.error(error);});

值得注意的是,调用resolve或reject当前,Promise 的使命就实现了,后继操作应该放到then办法外面,而不应该间接写在resolve或reject的前面。所以,最好在它们后面加上return语句,这样就不会有意外。

new Promise((resolve, reject) => {  return reject(1);  // 前面的语句不会执行  console.log(2);})

3、Promise.all()

let p1 = Promise.resolve(1)let p2 = new Promise(resolve => {  setTimeout(() => {    resolve(2)  }, 1000)})let p3 = Promise.resolve(3)Promise.all([p3, p2, p1])  .then(result => { // 返回的后果是依照Array中编写实例的程序来    console.log(result) // [ 3, 2, 1 ]  })  .catch(reason => {    console.log("失败:reason")  })

Promise.all 生成并返回一个新的 Promise 对象,所以它能够应用 Promise 实例的所有办法。参数传递promise数组中所有的 Promise 对象都变为resolve的时候,该办法才会返回, 新创建的 Promise 则会应用这些 promise 的值。

如果参数中的任何一个promise为reject的话,则整个Promise.all调用会立刻终止,并返回一个reject的新的 Promise 对象。

4、Promise.allSettled()

有时候,咱们不关怀异步操作的后果,只关怀这些操作有没有完结。这时,ES2020 引入Promise.allSettled()办法就很有用。如果没有这个办法,想要确保所有操作都完结,就很麻烦。Promise.all()办法无奈做到这一点。

如果有这样的场景:一个页面有三个区域,别离对应三个独立的接口数据,应用 Promise.all 来并发申请三个接口,如果其中任意一个接口出现异常,状态是reject,这会导致页面中该三个区域数据全都无奈进去,显然这种情况咱们是无奈承受,Promise.allSettled的呈现就能够解决这个痛点:

Promise.allSettled([  Promise.reject({ code: 500, msg: '服务异样' }),  Promise.resolve({ code: 200, list: [] }),  Promise.resolve({ code: 200, list: [] })]).then(res => {  console.log(res)  /* 0: {status: "rejected", reason: {…}} 1: {status: "fulfilled", value: {…}} 2: {status: "fulfilled", value: {…}} */  // 过滤掉 rejected 状态,尽可能多的保障页面区域数据渲染  RenderContent(    res.filter(el => {      return el.status !== 'rejected'    })  )})

Promise.allSettled跟Promise.all相似, 其参数承受一个Promise的数组, 返回一个新的Promise, 惟一的不同在于, 它不会进行短路, 也就是说当Promise全副解决实现后,咱们能够拿到每个Promise的状态, 而不论是否解决胜利。

5、Promise.race()

Promise.all()办法的成果是"谁跑的慢,以谁为准执行回调",那么绝对的就有另一个办法"谁跑的快,以谁为准执行回调",这就是Promise.race()办法,这个词原本就是赛跑的意思。race的用法与all一样,接管一个promise对象数组为参数。

Promise.all在接管到的所有的对象promise都变为FulFilled或者Rejected状态之后才会持续进行前面的解决,与之绝对的是Promise.race只有有一个promise对象进入FulFilled或者Rejected状态的话,就会持续进行前面的解决。

// `delay`毫秒后执行resolvefunction timerPromisefy(delay) {    return new Promise(resolve => {        setTimeout(() => {            resolve(delay);        }, delay);    });}// 任何一个promise变为resolve或reject的话程序就进行运行Promise.race([    timerPromisefy(1),    timerPromisefy(32),    timerPromisefy(64)]).then(function (value) {    console.log(value);    // => 1});

下面的代码创立了3个promise对象,这些promise对象会别离在1ms、32ms 和 64ms后变为确定状态,即FulFilled,并且在第一个变为确定状态的1ms后,.then注册的回调函数就会被调用。

6、Promise.prototype.finally()

ES9 新增 finally() 办法返回一个Promise。在promise完结时,无论后果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否胜利实现后都须要执行的代码提供了一种形式。这防止了同样的语句须要在then()和catch()中各写一次的状况。

比方咱们发送申请之前会呈现一个loading,当咱们申请发送实现之后,不论申请有没有出错,咱们都心愿关掉这个loading。

this.loading = truerequest()  .then((res) => {    // do something  })  .catch(() => {    // log err  })  .finally(() => {    this.loading = false  })

finally办法的回调函数不承受任何参数,这表明,finally办法外面的操作,应该是与状态无关的,不依赖于 Promise 的执行后果。

理论利用

假如有这样一个需要:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯一直交替反复亮灯?
三个亮灯函数曾经存在:

function red() {    console.log('red');}function green() {    console.log('green');}function yellow() {    console.log('yellow');}

这道题简单的中央在于须要“交替反复”亮灯,而不是亮完一遍就完结的一锤子买卖,咱们能够通过递归来实现:

// 用 promise 实现let task = (timer, light) => {  return new Promise((resolve, reject) => {    setTimeout(() => {      if (light === 'red') {        red()      }      if (light === 'green') {        green()      }      if (light === 'yellow') {        yellow()      }      resolve()    }, timer);  })}let step = () => {  task(3000, 'red')    .then(() => task(1000, 'green'))    .then(() => task(2000, 'yellow'))    .then(step)}step()

同样也能够通过async/await 的实现:

//  async/await 实现let step = async () => {  await task(3000, 'red')  await task(1000, 'green')  await task(2000, 'yellow')  step()}step()

应用 async/await 能够实现用同步代码的格调来编写异步代码,毫无疑问,还是 async/await 的计划更加直观,不过深刻了解Promise 是把握async/await的根底。