乐趣区

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

从上图能够看出 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,对于链式调用、值穿透个性等还没有实现。

参考链接

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