Promise/A+ 核心

Promise 札记 - 图1

在实现一个符合 Promise/A+ 规范的 promise 之前,先了解下 Promise/A+ 核心,想更全面地了解可以阅读 Promise/A+规范

  • Promise 操作只会处在 3 种状态的一种:未完成态(pending)、完成态(resolved) 和失败态(rejected);

  • Promise 的状态只会出现从未完成态向完成态或失败态转化;

  • Promise 的状态一旦转化,将不能被更改;

promise api

Promise.resolve()

Promise.resolve() 括号内有 4 种情况

  1. /* 跟 Promise 对象 */
  2. Promise.resolve(Promise.resolve(1))
  3. // Promise {state: "resolved", data: 1, callbackQueue: Array(0)}
  4. /* 跟 thenable 对象 */
  5. var thenable = {
  6. then: function(resolve, reject) {
  7. resolve(1)
  8. }
  9. }
  10. Promise.resolve(thenable)
  11. // Promise {state: "resolved", data: 1, callbackQueue: Array(0)}
  12. /* 普通参数 */
  13. Promise.resolve(1)
  14. // Promise {state: "resolved", data: 1, callbackQueue: Array(0)}
  15. /* 不跟参数 */
  16. Promise.resolve()
  17. // Promise {state: "resolved", data: undefined, callbackQueue: Array(0)}

Promise.reject()

相较于 Promise.resolve(),Promise.reject() 原封不动地返回参数值

Promise.all(arr)

对于 Promise.all(arr) 来说,在参数数组中所有元素都变为决定态后,然后才返回新的 promise。

  1. // 以下 demo,请求两个 url,当两个异步请求返还结果后,再请求第三个 url
  2. const p1 = request(`http://some.url.1`)
  3. const p2 = request(`http://some.url.2`)
  4. Promise.all([p1, p2])
  5. .then((datas) => { // 此处 datas 为调用 p1, p2 后的结果的数组
  6. return request(`http://some.url.3?a=${datas[0]}&b=${datas[1]}`)
  7. })
  8. .then((data) => {
  9. console.log(msg)
  10. })

Promise.race(arr)

对于 Promise.race(arr) 来说,只要参数数组有一个元素变为决定态,便返回新的 promise。

  1. // race 译为竞争,同样是请求两个 url,当且仅当一个请求返还结果后,就请求第三个 url
  2. const p1 = request(`http://some.url.1`)
  3. const p2 = request(`http://some.url.2`)
  4. Promise.race([p1, p2])
  5. .then((data) => { // 此处 data 取调用 p1, p2 后优先返回的结果
  6. return request(`http://some.url.3?value=${data}`)
  7. })
  8. .then((data) => {
  9. console.log(data)
  10. })

Promise.wrap(fn) —— 回调函数转 Promise

通过下面这个案例,提供回调函数 Promise 化的思路。

  1. function foo(a, b, cb) {
  2. ajax(
  3. `http://some.url?a=${a}&b=${b}`,
  4. cb
  5. )
  6. }
  7. foo(1, 2, function(err, data) {
  8. if (err) {
  9. console.log(err)
  10. } else {
  11. console.log(data)
  12. }
  13. })

如上是一个传统回调函数使用案例,只要使用 Promise.wrap() 包裹 foo 函数就对其完成了 promise 化,使用如下:

  1. const promiseFoo = Promise.wrap(foo)
  2. promiseFoo(1, 2)
  3. .then((data) => {
  4. console.log(data)
  5. })
  6. .catch((err) => {
  7. console.log(err)
  8. })

Promise.wrap 的实现逻辑也顺带列出来了:

  1. Promise.wrap = function(fn) {
  2. return funtion() {
  3. const args = [].slice.call(arguments)
  4. return new Promise((resolve, reject) => {
  5. fn.apply(null, args.concat((err, data) => {
  6. if (err) {
  7. reject(err)
  8. } else {
  9. resolve(data)
  10. }
  11. }))
  12. })
  13. }
  14. }

then/catch/done

这几个 api 比较简单,合起来一起带过

  1. Promise.resolve(1)
  2. .then((data) => {console.log(data)}, (err) => {console.log(err)}) // 链式调用,可以传一个参数(推荐),也可以传两个参数
  3. .catch((err) => {console.log(err)}) // 捕获链式调用中抛出的错误 || 捕获变为失败态的值
  4. .done() // 能捕获前面链式调用的错误(包括 catch 中),可以传两个参数也可不传

动手实现一个 Promise

实践了一个符合 Promise/A+ 规范的 repromise

坑点 1:事件循环

事件循环:同步队列执行完后,在指定时间后再执行异步队列的内容。

之所以要单列事件循环,因为代码的执行顺序与其息息相关,此处用 setTimeout 来模拟事件循环;

下面代码片段中,① 处执行完并不会马上执行 setTimeout() 中的代码(③),而是此时有多少次 then 的调用,就会重新进入 ② 处多少次后,再进入 ③

  1. excuteAsyncCallback(callback, value) {
  2. const that = this
  3. setTimeout(function() {
  4. const res = callback(value) // ③
  5. that.excuteCallback('fulfilled', res)
  6. }, 4)
  7. }
  8. then(onResolved, onRejected) {
  9. const promise = new this.constructor()
  10. if (this.state !== 'PENDING') {
  11. const callback = this.state === 'fulfilled' ? onResolved : onRejected
  12. this.excuteAsyncCallback.call(promise, callback, this.data) // ①
  13. } else {
  14. this.callbackArr.push(new CallbackItem(promise, onResolved, onRejected)) // ②
  15. }
  16. return promise
  17. }

坑点 2:this 的指向问题

this.callbackArr.push() 中的 this 指向的是 ‘上一个’ promise,所以类 CallbackItem 中,this.promise 存储的是’下一个’ promise(then 对象)。

  1. class Promise {
  2. ...
  3. then(onResolved, onRejected) {
  4. const promise = new this.constructor()
  5. if (this.state !== 'PENDING') { // 第一次进入 then,状态是 RESOLVED 或者是 REJECTED
  6. const callback = this.state === 'fulfilled' ? onResolved : onRejected
  7. this.excuteAsyncCallback.call(promise, callback, this.data) // 绑定 this 到 promise
  8. } else { // 从第二次开始以后,进入 then,状态是 PENDING
  9. this.callbackArr.push(new CallbackItem(promise, onResolved, onRejected)) // 这里的 this 也是指向‘上一个’ promise
  10. }
  11. return promise
  12. }
  13. ...
  14. }
  15. class CallbackItem {
  16. constructor(promise, onResolve, onReject) {
  17. this.promise = promise // 相应地,这里存储的 promise 是来自下一个 then 的
  18. this.onResolve = typeof(onResolve) === 'function' ? onResolve : (resolve) => {}
  19. this.onReject = typeof(onRejected) === 'function' ? onRejected : (rejected) => {}
  20. }
  21. ...
  22. }

拓展延伸

  1. setTimeout(() => {
  2. console.log('A')
  3. }, 0)
  4. Promise.resolve(
  5. console.log('B')
  6. ).then(() => {
  7. console.log('C')
  8. })
  9. console.log('D')

正常情况下,此 demo 应该输出 B D C A, 这里涉及到宏任务和微任务的知识点,一个宏任务里可以有多个微任务。

  • 宏任务(macroTask):setTimeout

  • 微任务(microTask):promise

由于此项目中的 promise 是用 setTimeout 实现的,所以在上述 demo 中,此项目输出的结果是 B D A C, 解决方法:可以使用 setImmediate 替代 setTimeout,可以参考 setImmediate.js。它的本质用了一些 hack 的手段,比如借用了 postMessage 这个来操作事件循环。

思考: promise.all 如何做到让多个 setTimeout 并发运行?

这个就是 promise.all() 的本质了,浏览器内部提供了一个事件循环机制来模拟成伪’并发’

  1. var oldTime = Date.now()
  2. setTimeout(() => {console.log(Date.now() - oldTime)}, 1000) // 1001 ~ 1005(存在 4ms 的波动)
  3. setTimeout(() => {console.log(Date.now() - oldTime)}, 2000) // 2001 ~ 2005