对于Promise的定义和根本应用,可参考红宝书和MDN。

在弄清楚Promise为何物之前,首先要明确它为何存在:

  • Promise不是新的语法,而是对回调函数这种异步编程的形式进行的改良。
  • Promise将嵌套调用改为链式调用,减少了可浏览性和可维护性;

Promise与回调函数

先说论断:回调函数是JS实现异步编程的形式之一,而Promise是解决回调天堂的形式之一。

在JavaScript的世界中,所有代码都是单线程执行的。因为这个“缺点”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行

以网络申请为例,如果须要在获取前一个申请的数据之后,再发动下一个申请,那么可能会写成如下模式:

ajax1(url1, () => {  doSomething1()  ajax2(url2, () => {    doSomething2()      ajax3(url3, () => {        doSomething3()        })    })})

如此上来,如果嵌套更多回调函数,就会造成常说的“回调天堂”

回调天堂的毛病很显著:

  • 代码耦合,浏览性差,不好保护;
  • 无奈应用try catch,就无奈排错。

而Promise能够很好的解决“回调天堂”问题:

ajax1(url1).then(res => {    doSomething1()  return ajax2(url2)}).then(res => {    doSomething2()  return ajax3(url3)}).then(res => {    doSomething3()}).catch(err => {  console.log(err)})

能够看到Promise的长处有:

  • 将回调函数的嵌套调用改为链式调用,代码好看;
  • 链式调用过程中如果出错,会进入catch办法,捕捉谬误;
  • Promise还提供了其余弱小的性能,比方:race、all等;

用Promise改写回调函数

在应用第三方提供的API时,如果该API是用回调函数写的,能够用Promise进行改写。

比方微信小程序发送申请的API:

wx.request({    url: '', // 申请的门路    method: "", // 申请的形式    data: {}, // 申请的数据    header: {}, // 申请头    success: (res) => {          // res  响应的数据    }})

上面应用Promise改写,即在胜利回调中resolve、在失败回调中reject:

function myrequest(options) {  return new Promise((resolve, reject) => { //创立Promise    wx.request({      url: options.url,      method: options.method || "GET",      data: options.data || {},      header: options.header || {},      success: res => {        resolve(res) //在胜利回调中resolve      },      fail: err => {        reject(err) //在失败回调中reject      }    })  })}

应用该自定义API:

myrequest({  url: 'xxx',  header: {    'content-type': 'json'  }}).then(res => {  console.log(res)}).catch(err => {  console.log(err)})

Promise的基本概念

Promise是ES6新增的对象,通过new来实例化,实例化时传入一个执行器函数(executor)作为参数:

// 执行器函数有两个参数:resolve、reject,它们也是函数const promise = new Promise(function(resolve, reject) {  // ... some code  if (/* 异步操作胜利 */){    resolve(value)  } else {    reject(error)  }})

Promise的特点有:

  • 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态pending(进行中)、fulfilled(已胜利)和rejected(已失败)。只有异步操作的后果,能够决定以后是哪一种状态,任何其余操作都无奈扭转这个状态;
  • 一旦状态扭转,就不会再变,任何时候都能够失去这个后果。Promise对象的状态扭转,只有两种可能:从pending变为fulfilled和从pending变为rejected。只有这两种状况产生,状态就凝固了,不会再变了,会始终放弃这个后果,这时就称为 resolved(已定型);

Promise的三种状态

  • Pending:期待;
  • Fulfilled:实现,调用resolve;
  • Rejected:回绝,调用reject;

从上图能够看出Promise的生命周期:

  • Promise的初始状态的是Pending;
  • 在创立Promise时就定义好何时resolve、何时reject;
  • then办法接管resolve的后果,而catch接管reject的后果,此时Promise状态为Fulfilled或Rejected;
  • then、catch办法又会返回新的Promise,从而实现链式调用;

Promise的链式调用

Promise的链式调用是如何实现的呢?先来看看Promise链式调用的个别写法:

new Promise((resolve, reject) => {    setTimeout(() => {        resolve()    })}).then(res => {    //自行处理    ...    res = res + '111'    //交给下一层解决    return res}).then(res => {    //自行处理    ...    res = res + '222'    //交给下一层解决    return res})

依照上图,then办法应该返回一个Promise对象,能力持续调用then/catch办法,然而这里间接return res为什么也行?

因为在then办法外部会主动将返回值包装成Promise,所以上述代码等价于:

new Promise((resolve, reject) => {    setTimeout(() => {        resolve()    })}).then(res => {    //自行处理    ...    res = res + '111'    //交给下一层解决    return Promise.resolve(res)}).then(res => {    //自行处理    ...    res = res + '222'    //交给下一层解决    return Promise.resolve(res)})
Promise.resolve(res)new Promise(resolve => {resolve(res)})的语法糖。

Promise与微工作

Promise中的执行函数是同步进行的,然而外面可能存在着异步操作,在异步操作完结后会调用resolve办法,或者中途遇到谬误调用reject办法,这两者都是作为微工作进入到事件循环中。那么,Promise为什么要引入微工作的形式来进行回调操作?

如何解决异步回调,有2种形式:

  1. 将回调函数放在宏工作队列的队尾。
  2. 将回调函数放到以后宏工作中的最初面(即作为微工作)。
  • 如果采纳第一种形式,那么执行回调(resolve/reject)的机会应该是在后面所有的宏工作实现之后,假使当初的工作队列十分长,那么回调迟迟得不到执行,造成利用卡顿
  • 为了解决上述计划的问题,另外也思考到提早绑定的需要,Promise 采取第二种形式,引入微工作,即把resolve/ reject回调的执行放在以后宏工作的开端;

Promise的执行程序

实际上要想搞清楚Promise的执行程序,就是了解Promise是如何进入事件循环的。

前置常识:

1:每一个当下正在被执行的JS代码是放在JS的主线程中的。同步的代码会依照代码程序顺次放入主栈,而后依照放入的程序顺次执行。

2:异步的代码会被放入微工作/宏工作队列,promise属于微工作。

3:异步的代码肯定是要等到同步的代码执行完了才执行。也就是说,直到JS Stack为空,微工作队列外面的代码才会被放入主栈,而后被执行。

4:new Promise()和.then()办法属于同步代码

5:.then(resolveCallback, rejectCallback)外面的resolveCallback, rejectCallback的执行属于异步代码,会被放入微工作队列

6: resolve()被调用会起到两点作用

  • Promise由pending状态变为resolved;
  • 遍历这个promise上所注册的所有的resolveCallback办法,顺次退出微工作队列

7: .then()只是注册callback办法,并不会把callback办法退出微工作队列(参考下面的第6点)。

来看几个例子:

例子一

new Promise((resolve, reject)=> {    console.log(4)  resolve(1)  Promise.resolve().then(()=>{        console.log(2)  })  }).then((t)=>{console.log(t)})console.log(3)//输入为:4 3 2 1

剖析:

  • new Promise的代码是同步执行的,所以其参数,即执行器函数(resolve, reject)=>{}是同步执行的,所以打印4是立刻执行的;
  • resolve(1)会把外层pomise状态由pending变成resolved,然而因为还没执行到外层then,所以此刻最外层的promise上并没有注册任何的callback办法,也就无奈把(t)=>{console.log(t)}退出微工作队列;
  • Promise.resolve()的后果曾经是resolved了,所以外部then的回调(打印2)间接退出微工作队列;
  • 最初才轮到外层then的回调(打印1)退出微工作队列;
  • 此时主栈和微工作队列:

    JS Stack:  [打印4,打印3]Microtask: [打印2,打印1]

例子二

new Promise((resolve, reject)=>{      Promise.resolve().then(()=>{ // cb1        resolve(1)        Promise.resolve().then(()=>{console.log(2)}) // cb2     })  }).then((value)=>{console.log(value)}) // cb3console.log(3)//输入:3 1 2

剖析:

  • 第2行then的回调(cb1)立刻退出微工作队列;
    此时:

    JS Stack:  [打印3]Microtask: [cb1]
  • 宏工作执行完就开始执行微工作(只有一个),先执行resolve(1),此时外层promise变成resolved,所以能够执行外层then了,将外层then的回调(cb3)退出微工作队列;

    此时:

    JS Stack:  []Microtask: [cb3]
  • 接着执行第4行,间接将cb2退出微工作队列;

    此时:

    JS Stack:  [cb3]Microtask: [cb2]

例子三

new Promise((resolve, reject)=>{      Promise.resolve().then(()=>{ // cb1          resolve(1);          Promise.resolve().then(()=>{console.log(2)}) // cb2      })      Promise.resolve().then(()=>{console.log(4)}) // cb3}).then((t)=>{console.log(t)}) // cb4  console.log(3);//输入:3 4 1 2

剖析:

  • 第2行和第6行then的回调(cb1、cb3)立刻退出微工作队列;
    此时:

    JS Stack:  [打印3]Microtask: [cb1, cb3]
  • 宏工作执行完就开始执行微工作(只有一个),先执行resolve(1),此时外层promise变成resolved,所以能够执行外层then了,将外层then的回调(cb4)退出微工作队列;

    此时:

    JS Stack:  [cb3]Microtask: [cb4]
  • 先执行主栈,打印4。接着执行第4行,间接将cb2退出微工作队列;

    此时:

    JS Stack:  []Microtask: [cb2]

Promise和async/await

通过以上剖析,Promise的链式调用是对于“回调天堂”的优化,然而如果链式调用太长,也不够好看。所以async/await就是进一步来优化then链的。

如果有三个步骤,每一个步骤都须要之前步骤的后果:

function takeLongTime(n) {    return new Promise(resolve => {        setTimeout(() => resolve(n + 200), n)    })}function step1(n) {    console.log(`step1 with ${n}`)    return takeLongTime(n)}function step2(n) {    console.log(`step2 with ${n}`)    return takeLongTime(n)}function step3(n) {    console.log(`step3 with ${n}`)    return takeLongTime(n)}

Promise链式调用会这么些:

function doIt() {    console.time("doIt")    const time1 = 300    step1(time1)        .then(time2 => step2(time2))        .then(time3 => step3(time3))        .then(result => {            console.log(`result is ${result}`)            console.timeEnd("doIt")        })}doIt()

如果用 async/await 来实现:

async function doIt() {    console.time("doIt")    const time1 = 300    const time2 = await step1(time1)    const time3 = await step2(time2)    const result = await step3(time3)    console.log(`result is ${result}`)    console.timeEnd("doIt")}doIt()

后果和之前的 Promise 实现是一样的,然而代码显得很简洁,看上去跟同步代码一样

上面来看看对于async/await的了解:

  • async 用于申明一个 function 是异步的,而 await 用于期待一个异步办法执行实现;
  • async 是一个修饰符,async 定义的函数会默认的返回一个Promise对象resolve的值,如果在函数中return一个间接量,async 会把这个间接量通过 Promise.resolve() 封装成 Promise 对象;
  • await 期待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有非凡限定);
  • 如果await等到的是一个 Promise 对象,它会阻塞前面的代码,等着 Promise 对象 resolve,而后失去 resolve 的值,作为 await 表达式的运算后果。

    所以,能够将所有Promise的链式调用都转换成async/await的模式。

手写Promise

如果能手写出Promise,那么对其原理的了解天然就会粗浅了。

想要手写一个 Promise,就要遵循Promise/A+ 标准,业界所有Promise的类库都遵循这个标准。

联合Promise/A+标准,能够剖析出Promise的基本特征:

  1. promise 有三个状态:pendingfulfilled,or rejected;「标准 Promise/A+ 2.1」
  2. new promise时, 须要传递一个executor()执行器,执行器立刻执行;
  3. executor 承受两个参数,别离是resolvereject
  4. promise 的默认状态是 pending
  5. promise 有一个value保留胜利状态的值,能够是undefined/thenable/promise;「标准 Promise/A+ 1.3」
  6. promise 有一个reason保留失败状态的值;「标准 Promise/A+ 1.5」
  7. promise 只能从pendingrejected, 或者从pendingfulfilled,状态一旦确认,就不会再扭转;
  8. promise 必须有一个then办法,then 接管两个参数,别离是 promise 胜利的回调 onFulfilled, 和 promise 失败的回调 onRejected;「标准 Promise/A+ 2.2」
  9. 如果调用 then 时,promise 曾经胜利,则执行onFulfilled,参数是promisevalue
  10. 如果调用 then 时,promise 曾经失败,那么执行onRejected, 参数是promisereason
  11. 如果 then 中抛出了异样,那么就会把这个异样作为参数,传递给下一个 then 的失败的回调onRejected

实现Promise如下:

// Promise的三种状态const PENDING = 'PENDING';const FULFILLED = 'FULFILLED';const REJECTED = 'REJECTED';// 自定义MyPromise类class MyPromise{  constructor(executor){    this.status = PENDING    this.value = undefined    this.reason = undefined    // 寄存胜利的回调    this.onResolvedCallbacks = []    // 寄存失败的回调    this.onRejectedCallbacks = []        let resolve = (value) => {      if(this.status === PENDING){        this.status = FULFILLED        this.value = value         // 顺次将对应的函数执行        this.onResolvedCallbacks.forEach(fn=>fn())      }    }    let reject = (reason) => {      if(this.status === PENDING){        this.status = REJECTED        this.reason = reason         // 顺次将对应的函数执行        this.onRejectedCallbacks.forEach(fn=>fn())      }    }        try{      executor(resolve, reject)    }catch(err){      reject(err)    }  }    // then办法  then(onFulfilled, onRejected) {    if (this.status === FULFILLED) {      onFulfilled(this.value)    }    if (this.status === REJECTED) {      onRejected(this.reason)    }    // 如果promise的状态是 pending,须要将 onFulfilled 和 onRejected 函数寄存起来,期待状态确定后,再顺次将对应的函数执行    if (this.status === PENDING) {      this.onResolvedCallbacks.push(() => {        onFulfilled(this.value)      })      this.onRejectedCallbacks.push(()=> {        onRejected(this.reason)      })    }  }}

应用自定义的MyPromise:

const promise = new MyPromise((resolve, reject) => {  setTimeout(()=>{    resolve('胜利');  },1000)}).then(  (res) => {    console.log('success', res)  },  (err) => {    console.log('faild', err)  })
留神,以上只是实现了简易版Promise,对于链式调用、值穿透个性等还没有实现。

参考链接

  • Javascript异步编程的4种办法
  • 为什么Promise要引入微工作?
  • Promise 对象——阮一峰
  • 了解 JavaScript 的 async/await
  • JS - Promise的执行程序