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;
JS中关于Promise的一切
文章图片

从上图可以看出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

【JS中关于Promise的一切】分析:
  • 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 = https://www.it610.com/article/undefined this.reason = undefined // 存放成功的回调 this.onResolvedCallbacks = [] // 存放失败的回调 this.onRejectedCallbacks = []let resolve = (value) => { if(this.status === PENDING){ this.status = FULFILLED this.value = https://www.it610.com/article/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的执行顺序

    推荐阅读