手写Promise,实现Promises/A+规范,实现all、race、finally、allSettlte、any等方法的实现

580 阅读10分钟

前言

在平时工作中,我们经常用promise来解决异步回调问题。你知道promise是如何实现的吗?
本文采用promises/A+规范来实现promise。
这是Promises/A+规范文档。 这是中文翻译

源码仓库:github

基本状态问题

首先我们来开一个简单的promise的例子:

new Promise((resolve, reject) => {
  resolve('ok')
}).then((value) => {
  console.log(value) // ok
})

通过这个例子我们来了解几个promise的概念

    1. 当使用promise时,会传入一个执行器,执行器传入两个函数(resolve, reject),此执行器是立即执行的;
    1. 有三种状态:成功(fulfilled),失败(rejected),等待(pending);
    1. 执行器调用resolve走成功态,如果调用reject或发生异常,走失败态;如果执行器抛异常,走失败态;
    1. promise状态一旦改变,以后不能更改
  1. 我们首先来定义一个最基础的结构。
// promise的三种状态
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'

class Promise {
  // 传入一个执行器
  constructor(executor) {
    const resolve = (value) => {}
    const reject = (reason) => {}
    // 该执行是立即执行的,并且会传入两个函数resolve, reject
    executor(resolve, reject)
  }
  then(onFulfilled, onRejected) {}
}
  1. 接着,实现改变状态。

// promise的三种状态
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
class Promise {
  // 传入一个执行器
  constructor(executor) {
    // 初始状态是等待状态
    this.state = PENDING
    const resolve = (value) => {
      /**
       * 1. 调用resolve方法状态会改为FULFILLED
       * 2. 由于promise状态一旦改变,以后不能更改,所以只有在PENDING状态时才能改为FULFILLED
       */
      if (this.state === PENDING) {
        this.state = FULFILLED
        // 成功态时的数据
        this.value = value
      }
    }
    const reject = (reason) => {
      /**
       * 1. 调用resolve方法状态会改为REJECTED
       * 2. 由于promise状态一旦改变,以后不能更改,所以只有在PENDING状态时才能改为REJECTED
       */
      if (this.state === PENDING) {
        this.state = REJECTED
        // 失败态时的原因
        this.reason = reason
      }
    }
    /**
     * 该执行是立即执行的,并且会传入两个函数resolve, reject。
     * 如果执行器抛异常,走失败态。所以加上try/catch
     */
    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
  then(onFulfilled, onRejected) {
    // 根据状态调用不同的回调函数
    if (this.state === FULFILLED) {
      onFulfilled(this.value)
    }
    if (this.state === REJECTED) {
      onRejected(this.reason)
    }
  }
}

到这里,我们实现了最基本的promise。

promise异步问题

以上代码,有什么问题吗?请看下面的例子,我们使用setTimeout

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('ok')
  }, 1000)
}).then((value) => {
  console.log(value) // ok
})

此时,发现then方法的回调没有打印ok。原因就是因为setTimeout是异步方法。那如何解决这个问题? 我们这时就可以用发布订阅的思想来解决。因为promise的状态改变可能是异步方法里面,所以可以在then方法里,如果当前状态是pending状态时先收集回调,然后等状态改变的时候(调用resolve或reject时),将一系列回调方法执行。


// promise的三种状态
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
class Promise {
  // 传入一个执行器
  constructor(executor) {
    // 初始状态是等待状态
    this.state = PENDING
    this.resolvedCallbacks = [] // 存放成功的回调
    this.rejectedCallbacks = [] // 存放失败的回调
    const resolve = (value) => {
      /**
       * 1. 调用resolve方法状态会改为FULFILLED
       * 2. 由于promise状态一旦改变,以后不能更改,所以只有在PENDING状态时才能改为FULFILLED
       */
      if (this.state === PENDING) {
        this.state = FULFILLED
        // 成功态时的数据
        this.value = value
        // 异步回调
        this.resolvedCallbacks.forEach(fn => fn())
      }
    }
    const reject = (reason) => {
      /**
       * 1. 调用resolve方法状态会改为REJECTED
       * 2. 由于promise状态一旦改变,以后不能更改,所以只有在PENDING状态时才能改为REJECTED
       */
      if (this.state === PENDING) {
        this.state = REJECTED
        // 失败态时的原因
        this.reason = reason
        // 异步回调
        this.rejectedCallbacks.forEach(fn => fn())
      }
    }
    /**
     * 该执行是立即执行的,并且会传入两个函数resolve, reject。
     * 如果执行器抛异常,走失败态。所以加上try/catch
     */
    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
  then(onFulfilled, onRejected) {
    // 根据状态调用不同的回调函数
    if (this.state === FULFILLED) {
      onFulfilled(this.value)
    }
    if (this.state === REJECTED) {
      onRejected(this.reason)
    }
    // pending状态,异步调用,需要把回调存起来
    if (this.state === PENDING) {
      this.resolvedCallbacks.push(() => {
        onFulfilled(this.value)
      })
      this.rejectedCallbacks.push(() => {
        onRejected(this.reason)
      })
    }
  }
}

then方法的几个问题

    1. 链式调用
      then()方法返回的是一个新的promise,因此后面可继续调用then
new Promise((resolve, reject) => {
  resolve('ok')
}).then((value) => {
  console.log(value) // ok
  return 'abc'
}).then((value) => {
  console.log(value) // abc
})

    1. then(onFulfilled, onRejected)中,回调方法的返回值问题:
      1. 返回普通值(x),则直接传递下一个then中的onFulfilled,即直接resolve(x)
      1. 返回Promise,则需要根据Promise的状态来决定下一个then是使用onFulfilled还onRejected回调
new Promise((resolve, reject) => {
  resolve('ok')
}).then((value) => {
  console.log(value) // ok
  return new Promise((resolve, reject) => {
    reject('123')
  })
}).then(null, (value) => {
  console.log(value) // 123
})
    1. 透传问题,then方法的两个回调onFulfilled, onRejected可能都为空
new Promise((resolve, reject) => {
  resolve('ok')
}).then().then().then().then((value) => {
  console.log(value) // ok
})

解决链式调用和then回调中返回普通值的问题

then(onFulfilled, onRejected) {
    const promise2 = new Promise((resolve, reject) => {
      // 根据状态调用不同的回调函数
      if (this.state === FULFILLED) {
        // 返回普通值(x),则直接传递下一个then中的onFulfilled,即直接resolve(x), 下同
        let x = onFulfilled(this.value)
        resolve(x)
      }
      if (this.state === REJECTED) {
        let x = onRejected(this.reason)
        resolve(x)
      }
      // pending状态,异步调用,需要把回调存起来
      if (this.state === PENDING) {
        this.resolvedCallbacks.push(() => {
          let x = onFulfilled(this.value)
          resolve(x)
        })
        this.rejectedCallbacks.push(() => {
          let x = onRejected(this.reason)
          resolve(x)
        })
      }
    })
    return promise2
}

then回调方法中返回的是promise。

首先把resolve(x)抽离成一个新的函数function resolvePromise(promise2, x, resolve, reject),该函数我们会解决以下几个问题:

    1. 如果 promise2 === x 需要reject一个类型错误。因为promise2 === x会死循环,想象一下,promise2在等待x的状态的改变,而x又是promise2,你在等我改变状态,我也在等你改变状态,是不是就陷入死循环了呢。
    1. 判断x是否为promise。首先x必须是对象或函数,然后还必须有then方法

resolvePromise函数

function resolvePromise(promise2, x, resolve, reject) {
  // 1. 如果promise2 === x,需要reject一个类型错误
  if (x === promise2) {
    return reject(new TypeError(`Chaining cycle detected for promise #<Promise>`))
  }
  // 是对象或函数
  if (x !== null && (typeof x === 'function' || typeof x === 'object')) {
    // 必须有then方法
    let then = x.then
    if (typeof then === 'function') {
      then.call(x, (y) => {
        resolve(y)
      }, (r) => {
        reject(r)
      })
    } else { // 没有then方法按普通值处理
      resolve(x)
    }
  } else { // 普通值
    resolve(x)
  }
}

then方法修改为:

then(onFulfilled, onRejected) {
    const promise2 = new Promise((resolve, reject) => {
      // 根据状态调用不同的回调函数
      if (this.state === FULFILLED) {
        // 这里需要加上的setTimeout,才能访问到promise2,下同
        setTimeout(() => {
          // 返回普通值(x),则直接传递下一个then中的onFulfilled,即直接resolve(x), 下同
          let x = onFulfilled(this.value)
          resolvePromise(promise2, x, resolve, reject)
        })
      }
      if (this.state === REJECTED) {
        setTimeout(() => {
          let x = onRejected(this.reason)
          resolvePromise(promise2, x, resolve, reject)
        })
      }
      // pending状态,异步调用,需要把回调存起来
      if (this.state === PENDING) {
        this.resolvedCallbacks.push(() => {
          setTimeout(() => {
            let x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          })
        })
        this.rejectedCallbacks.push(() => {
          setTimeout(() => {
            let x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          })
        })
      }
    })
    return promise2
  }

上面的resolvePromise函数,还有几个以下问题:

  1. 在取then方法的时候,可能报错,因此需要try/catch捕获并reject
  2. 保证then方法的回调只被调用一次
  3. y可能继续返回一个promise,因此需要使用递归,直到y返回普通值为止。 修改如下:
function resolvePromise(promise2, x, resolve, reject) {
  // 1. 如果promise2 === x,需要reject一个类型错误
  if (x === promise2) {
    return reject(new TypeError(`Chaining cycle detected for promise #<Promise>`))
  }
  // 是对象或函数
  if (x !== null && (typeof x === 'function' || typeof x === 'object')) {
    let called = false // 保证只调用一次
    try {
      // 必须有then方法
      let then = x.then
      if (typeof then === 'function') {
        then.call(x, (y) => {
          if (called) return
          called = true
          // resolve(y)
          resolvePromise(promise2, y, resolve, reject)
        }, (r) => {
          if (called) return
          called = true
          reject(r)
        })
      } else { // 没有then方法按普通值处理
        resolve(x)
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else { // 普通值
    resolve(x)
  }
}

透传问题

这个比较容易解决,直接上代码

then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
    const promise2 = new Promise((resolve, reject) => {
      .....
    return promise2
  }

then方法还有一个问题,就是onFulfilled,onRejected可能抛出异常,因此需要捕获。

then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
    const promise2 = new Promise((resolve, reject) => {
      // 根据状态调用不同的回调函数
      if (this.state === FULFILLED) {
        // 这里需要加上的setTimeout,才能访问到promise2,下同
        setTimeout(() => {
          try {
            // 返回普通值(x),则直接传递下一个then中的onFulfilled,即直接resolve(x), 下同
            let x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }
      if (this.state === REJECTED) {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }
      // pending状态,异步调用,需要把回调存起来
      if (this.state === PENDING) {
        this.resolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value)
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          })
        })
        this.rejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason)
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          })
        })
      }
    })
    return promise2
  }

测试

到这里,我们就完成了promises/A+规范了,如何测试了。

  1. 按照测试包promises-aplus-tests
npm i promises-aplus-tests -g
  1. 实现deferred
// promises-aplus-tests
Promise.deferred = function() {
  let dfd = {}
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}
module.exports = Promise
  1. 开始测试
    假设你的代码文件名为myPromise.js
promises-aplus-tests myPromise.js

promise-tests.png

perfect! 872个测试全部通过

promise的一些方法

在上面的实现promises/A+规范中,并没有规定resolve(promise),为了和es6的promise表现一致,修改修改一下:

const resolve = (value) => {
    // 下面的这个规范没有要求
    if (value instanceof Promise) {
    return value.then(resolve, reject)
    }
    /**
    * 1. 调用resolve方法状态会改为FULFILLED
    * 2. 由于promise状态一旦改变,以后不能更改,所以只有在PENDING状态时才能改为FULFILLED
    */
    if (this.state === PENDING) {
    this.state = FULFILLED
    // 成功态时的数据
    this.value = value
    // 异步回调
    this.resolvedCallbacks.forEach(fn => fn())
    }
}

Promise.resolve

Promise类方法,将一个对象转为一个Promise,状态为fulfilled

static resolve(data) {
    return new Promise((resolve) => {
      resolve(data)
    })
}

Promise.reject

Promise类方法,将一个对象转为一个Promise,状态为rejected

static reject(data) {
    return new Promise((resolve, reject) => {
      reject(data)
    })
}    

Promise.prototype.catch

catch方法是 Promise.prototype.then(undefined, onRejected) 的一种简写形式。返回一个新的promise

catch(fn) {
  return this.then(null, fn)
}

Promise.prototype.finally

此方法返回一个Promise。在 promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。 下面是finally的使用方法

new Promise((resolve, reject) => {
  resolve('ok')
}).then((value) => {
  return 'abc'
}).finally(() => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('def')
    }, 1000)
  })
}).then((value) => {
  // 会等待1s,但value 是abc不是def
  console.log(value) // abc
})

实现:

finally(fn) {
    return this.then((value) => {
      return Promise.resolve(fn()).then(() => value)
    }, (err) => {
      return Promise.resolve(fn()).then(() => { throw err })
    })
}

Promise.all

将多个Promise放在一个数组中,当整个数组的全部promise成功时才会返回成功, 当数组中的promise有一个出现失败时就返回失败 (失败的原因是第一个失败promise的结果)。
返回的是一个promise

先看一个使用的例子

const p1 = '1111'
const p2 = Promise.resolve('22222')
const p3 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('3333')
  }, 2000)
})
const p4 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('4444')
  }, 1000)
})
Promise.all([p1, p2, p3, p4]).then(value => {
  console.log(value) // [ '1111', '22222', '3333', '4444' ]
})

代码实现

static all(promises) {
    return new Promise((resolve, reject) => {
      let result = []
      let times = 0
      const processSuccess = (index, value) => {
        result[i] = value
        if (++times === promises.length) {
          resolve(result)
        }
      }
      for (let i = 0; i < promises.length; i++) {
        let p = promises[i]
        // 判断p是否为promise,即判断有没有then方法
        if (p && typeof p.then === 'function') {
          p.then((value) => {
            processSuccess(i, value)
          }, reject)
        } else {
          processSuccess(i, p)
        }
      }
    })
  }

Promise.race

该方法返回一个 Promise,它将与第一个传递的 promise 相同的完成方式被完成。 它可以是完成( resolves),也可以是失败(rejects),这要取决于第一个完成的方式是两个中的哪个。

实现

static race(promises) {
    return new Promise((resolve, reject) => {
      for (let i = 0; i < promises.length; i++) {
        const p = promises[i]
        if (p && typeof p.then === 'function') {
          p.then(resolve, reject)
        } else {
          resolve(p)
        }
      }
    })
  }

Promise.allSettled

返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。

比如:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve({
      data: 'aaaaa'
    })
  }, 1000)
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve({
      data: 'bbbbb'
    })
  }, 2000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('404')
  }, 1500)
})
const p4 = '1111'
Promise.allSettled([p1, p2, p3, p4]).then((value) => {
  console.log('value', value)
}, (reason) => {
  console('reason', reason)
})
/**
输出
value [

  { status: 'fulfilled', value: { data: 'aaaaa' } },

  { status: 'fulfilled', value: { data: 'bbbbb' } },

  { status: 'rejected', reason: '404' },

  { status: 'fulfilled', value: '1111' }

]
**/

代码实现

static allSettled(promises) {

    let result = []
    let times = 0

    return new Promise((resolve, reject) => {
      const processComplete = (index, data, state) => {
        result[index] = {
          state,
          ...data
        }
        if (++times === promises.length) {
          resolve(result)
        }
      }
      for (let i = 0; i < promises.length; i++) {
        const p = promises[i]
        if (p && typeof p.then === 'function') {
          p.then((value) => {
            processComplete(i, { value }, 'fulfilled')
          }, (reason) => {
            processComplete(i, { reason }, 'rejected')
          })
        } else {
          processComplete(i, { value: p }, 'fulfilled')
        }
      }
    })
  }

Promise.any

只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

注意:Promise.any() 方法依然是实验性的,尚未被所有的浏览器完全支持。

Promise.any()Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个 Promise 变成rejected状态而结束,必须等到所有参数 Promise 变成rejected状态才会结束。

代码实现如下

static any(promises) {
    let result = []
    let resolveValue
    let hasResolved = false
    let times = 0
    return new Promise((resolve, reject) => {
      const processFail = (index, reason) => {
        result[index] = reason
        if(++times === promises.length) {
          reject(new AggregateError(result, 'All promises were rejected'))
        }
      }
      const processSuccess = (index, value) => {
        if (!hasResolved) {
          resolveValue = value
          hasResolved = true
        }
        if(++times === promises.length) {
          resolve(resolveValue)
        }
      }
      for (let i = 0; i < promises.length; i++) {
        let p = promises[i]
        p.then((value) => {
          processSuccess(i, value)
        }, (reason) => {
          processFail(i, reason)
        })
      }
    })
  }