JS中关于Promise的一切

关于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;

1633422792494

从上图可以看出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)}) // cb3

console.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,对于链式调用、值穿透特性等还没有实现。

参考链接

你可能感兴趣的:(JS中关于Promise的一切)