实现了一下 Promise 源码

778 阅读13分钟

我们使用类的方式来实现一个 Promise

概念及作用

在《JavaScript 高级程序设计第四版》中是这样描述 Promise的:期约(也就是 Promise)是对尚不存在结果的一个替身。

在ES6 中增加引用数据类型 Promise,通过 new关键字来实例化。它需要传入一个执行器(executor)函数作为参数(必传,否则会出错)。

所以这里我们先定义一个 Promise类:

class Promise {
  constructor(executor) {
    if (isUndef(executor) || !isFunction(executor)) {
      throw new TypeError('Promise resolver undefined is not a function')
    }
  }
}

它有两个作用:

  • 抽象地表示一个异步操作

  • 状态代表 Promise是否完成

    • 成功就会有一个私有的内部值(value)
    • 拒绝就会有一个私有的内部理由(reason)

无论是值还是理由,都是包含原始值或者对象的不可修改的引用。二者是可选的,且默认值是 undefined

class Promise {
  constructor(executor) {
    if (isUndef(executor) || !isFunction(executor)) {
      throw new TypeError('Promise resolver undefined is not a function')
    }
    this.value = undefined;
    this.reason = undefined;
  }
}

状态

Promise是一个有状态的对象,一共三种状态:

  • pending待定:Promise 的初始状态
  • fulfilled完成
  • rejected拒绝

这三种状态的关系为:

  • pending-->fulfilled
  • pending-->rejected

这两种落定过程都是不可逆的。

Promise的状态是私有的,不能直接通过 JS 检测到。

那这里的状态实现如下:

const PENGDING = 'PENDING'
const FULLFILLED = 'FULLFILLED'
const REJECT = 'REJECT'

class Promise {
  constructor(executor) {
    if (isUndef(executor) || !isFunction(executor)) {
      throw new TypeError('Promise resolver undefined is not a function')
    }
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
  }
}

执行器函数

由于 Promise的状态是私有的,所以只能在内部进行操作。它将会在执行器函数中完成。执行器函数的职责有两个:

  • 初始化 Promise的异步行为
  • 控制状态的最终转换--它的实现又是通过调用它的两个函数参数实现的
    • resolve:调用后状态切换为完成;
    • reject:调用后状态切换为拒绝,并会抛出错误;
const PENGDING = 'PENDING'
const FULLFILLED = 'FULLFILLED'
const REJECT = 'REJECT'

class Promise {
  constructor(executor) {
    if (isUndef(executor) || !isFunction(executor)) {
      throw new TypeError('Promise resolver undefined is not a function')
    }
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    const resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULLFILLED
        this.value = value
      }
    }
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECT
        this.reason = reason
      }
    }
    
    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }
}

需要知道的是:执行器函数是同步执行的,这是因为执行器函数本身是 Promise的初始化程序

⚠️注意:
这里的 resolvereject方法使用箭头函数的意义,并不是为了简便,更重要的在于 this指向问题。

静态方法

这里只写几个常用的方法

Promise.resolve

Promise并非一开始就必须处于待定状态,然后调用执行器函数才能改变最终态。可以调用其静态 resolve()方法实例化一个已完成的 Promise

  • 使用这个方法可以将任何一个值都转换为一个 Promise
  • 如果传入了一个 Promise,那它的行为就类似于一个空包装,直接返回这个 Promise
  • 如果这个值是 thenable,即带有 then方法的值,返回的 promise对象会在其内部调用该值的 then方法,并将 resolvereject作为参数传入

⚠️注意:该方法能够包装任何非 Promise 值,包括错误对象,并将其转为已完成的 Promise

Promise.resolve = function (value) {
  if (value instanceof Promise) {
    return value
  }
  
  if (
    (value instanceof Object) &&
    (value.then) &&
    (typeof value.then === 'function')
  ) {
    return new Promise((resolve, reject) => {
      value.then(resolve, reject)
    })  
  }
  
  return new Promise(resolve => {
    resolve(value)
  })
}

Promise.reject

同上一个方法类似,它会实例化一个被拒绝的 Promise并抛出一个异步错误。且这个错误不能通过 try/catch进行捕获,只能通过 reject来捕获。

Promise.reject = function(reason) {
  return new Promise((resolve, reject) => {
    reject(reason)
  })
}

Promise.all

all主要是将一组 Promise全部解决完成之后再返回一个 Promise实例,结果为所有 Promise回调结果的集合(按照迭代器顺序)。

在 MDN 上我们可以知道该方法的返回值需要遵循以下三点:

  • 如果传入的参数是一个空的可迭代对象(可以理解为一个空数组之类的),那么返回一个已完成(already resolved)状态的 Promise。这里是一个同步的操作
  • 如果传入的参数包含不是 Promise的值,那么最终它将会被按照顺序放在最终的返回集合当中
  • 假如其中有一个 Promisereject掉了,那整个过程就会抛出异常,而异常信息就是第一个被 reject掉的信息。

reject之后再被 rejectPromise不会影响到最终的 reject reason。且其他 Promisereject操作都会被静默处理掉

Promise.all = function (values) {
  return new Promise((resolve, reject) => {
    const array = []
    let index = 0;
    
    if (!Array.isArray(values)) {
      reject(new TypeError('Argument is not iterable'))
    }
    
    if (values.length === 0) {
      return resolve(values)
    }
    
    function processData(key, data) {
      array[key] = data
      if (++index === values.length) {
        resolve(array)
      }
    }
    
    values.forEach((item, index) => {
      Promise.resolve(item).then(data => {
        processData(index, data)
      }, reject)
    })
  })
}

Promise.race

这个方法返回了一个包装 Promise,是一组集合中最先解决或者失败的 Promise镜像。它需要遵循以下两点:

  • 只要是第一个状态落定的 Promise,就会包装其解决值或者拒绝理由并返回一个新的 Promise
  • 如果传入的参数是空集合,那么返回的 Promise的状态将永远 Pending
  • 如果迭代包含一个或者多个非承诺值和/或已解决/拒绝的承诺,则 Promise.race将解析为迭代中找到的第一个值
Promise.race = function (values) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(values)) {
      reject(new TypeError('Argument is not iterable'))
    }
    
    if (values.length > 0) {
      values.forEach(item => {
        Promise.resolve(item).then(resolve, reject)
      })
    }
  })
}

实例方法

我们熟知的 .then.catch.finally方法都挂载在 Promise的原型对象上。我们一个个来看。

then

它可以说是 Promise上处理程序的主要方法,接收两个参数:

  • onResolved:即进入到完成状态时会调用的方法,传入值
  • obRejected:即进入到拒绝状态时会调用的方法,传入理由

注意这两个参数都是可选的。且传给 then函数的任何非函数类型的参数都会被静默忽略(即会把将值或者失败理由返回,毕竟是要处理数据的)。而如果只想提供 obRejected参数,那么就要在onResolved参数的位置上传入 null或者 undefined。这有助于避免在内存中创建出多余的对象出来。

Promise.prototype.then = function (onResolved, obRejected) {
  // 判断两个参数是可选参数
  onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : data => data
  onReject = typeof onReject === 'function' ? onReject : err => { throw err; }
  if (this.status === FULLFILLED) {
    onResolved(this.value)
  }
  if (this.status === REJECT) {
    obRejected(this.reason)
  }
}

上面这部分的代码块其实可以看出来:Promise只会执行 成功态拒绝态其中的一个。

而还需要注意的一个重点是:then方法会返回一个新的 Promise实例。

Promise.prototype.then = function (onResolved, obRejected) {
  return new Promise((resolve, reject) => {})
}

那么返回一个新的 Promise 实例也是 Promises/A+中的规范,也正因为有这一点,所以我们才可以使用链式调用。

而这个新的 Promise我们暂且将其称为 promise2,原来的成为 promise,规范中定义 promise1无论是 resolve还是 reject(返回一个值 x)都会执行 Promise 的解决过程:[[Resolve]](promise2, x),只有这俩里抛出异常才会被拒绝执行。

那这里的 Promise 解决过程要如何实现呢?

  • 首先根据规范我们可以知道这是一个抽象的操作,它主要针对 promise2promise1中落定状态的返回值 x之间的关系
  • 解析 promise2x之间的关系及 x的类型,判断使用 promise2里的 resolvereject
    • xpromise2相等,TypeError抛出异常

    • 如果 x是一个对象或者函数时,有以下几种场景:

      • 假设 x.then是一个函数,那么x就是一个 promise对象,针对这种场景,还需要防止多次调用成功和失败的方法
      • 返回它可能就是一个正常的函数或者对象,我们只需要直接 resolve掉即可
    • 如果 x 是一个普通值,则以 x为参数执行 promise

// 定义该方法名为:resolvePromise
/**
 * 根据 x 和 promise2,使用 promise2 里的 resolve 和 reject 完成 then 调用之后的逻辑
 * @param {Promise} promise2 promise1.then 返回的新 Promise 对象
 * @param {[type]} x promise1 中 onFullfilled 或者 onReject 的返回值
 * @param {[type]} resolve promise2 中的 resolve 方法 
 * @param {[type]} reject promise2 中的 reject 方法
 */
function resolvePromise(
  promise2,
  x,
  resolve,
  reject
) {
  let called;
  if ((typeof x === 'object' && x !== null) || (typeof x === 'function')) {
    try {
      const then = x.then;
      if (typeof then === 'function') {
        // 当 x 是 promise 的时候递归调用该方法
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject)
          },
          r => {
            if (called) return;
            reject(r)
          }
        )
      } else {
        if (called) return;
        called = true;
        resolve(x)
      }
    } catch (error) {
      if (called) return;
      called = true
      reject(error)
    }
  } else {
    if (called) return
    called = true
    resolve(x);
  }
}

完成之后,我们在异步的部分来使用。

catch

catch方法用于给 Promise添加一个拒绝处理程序。而它实际上就是一个语法糖,调用它就相当于调用 Promise.prototype.then(null, onRejected)。也就是说:

promise1.then(val => {
  console.log('then', val)
})
.catch(err => {
  console.log('catch', err)
})

// 等价于
promise1.then(
  null, // 或者 undefined
  error => { console.log(error) }
)

所以呢,catch的最终实现:

Promise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected)
}

finally

finally方法用于给 Promise添加 onFinally处理程序,这个处理程序在 Promise转换为完成或者拒绝状态时都会执行。

不过它没有办法知道 Promise 的状态是完成还是拒绝,所以这个方法主要用来添加清理代码。

onFinally方法被设计为一个与状态无关的方法,所以一般情况下它都会将其上一层的 Promise原样的返回出去。所以 onFinally方法是不接受任何参数的。

Promise.prototype.finally = function (cb) {
  return this.then(cb, cb)
}

可以看到其实在实现该方法的过程中我们并没有对当前的 Promise状态进行判断。因为此时,如果返回的是一个 PendingPromise或者 onFinally处理程序抛出了错误(显示地抛出或者返回了一个 reject),都会返回相应的 Promise

这种 Pending的场景其实并不常见,因为只要 Promise一解决,新的 Promise仍然会原样的返回初始的 Promise

const p1 = Promise.resolve('foo')

// 忽略完成的值
const p2 = p1.finally(
  () => new Promise((resolve, reject) => setTimeout(() => resolve('bar'), 100))
)

setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(() => setTimeout(console.log, 0, p2), 200)

// 200ms 后
// Promise <resolved>: foo

Promise 中的异步

then 中的异步

到现在为止,我们的 Promise都还只是同步的,并没有任何异步的部分,但是实际上原生的 Promisethen方法里的函数参数都会被包裹在一个异步操作中执行。

添加异步的原因也很明确,就是为了保证返回的新 Promise已经是处于生成状态的

Promise.prototype.then = function (onResolved, obRejected) {
  onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : data => data
  onReject = typeof onReject === 'function' ? onReject : err => { throw err; }
  return new Promise((resolve, reject) => {
    if (this.status === FULLFILLED) {
      setTimeout(() => {
        onResolved(this.value)
      })
    }
    if (this.status === REJECT) {
      setTimeout(() => {
        obRejected(this.reason)
      })
    }
  })
}

这里其实也是 Promises/A+规范里提到的(2.2.4):

onResolvedobRejected必须在执行环境堆栈仅包含平台代码时才能被调用。

在这里的平台代码指的是引擎、环境以及 Promise 的实施代码。它要确保这两个方法在调用时,是异步的,且应该在 then方法被调用的那一轮事件循环之后的新执行栈中进行。

这个事件队列可以采用宏任务(macro-task) 机制,比如 setTimeout或者 setImmediate,也可以使用微任务(micro-task) 机制来实现,比如 MutationObserver或者process.nextTick

而我们这里就使用了 setTimeout

还有一点需要注意的是,当 Promise的状态为 pending时,我们这里并没有兜底方案,所以这里应该还需要加入 pending状态时的处理:

  • pending状态时,我们可以把 then中的函数都存储到一个数组中(多次调用 then
  • 当状态落定转换之后,在对应的 resolvereject函数中循环执行数组中的事件
  • 这实际上是一个发布-订阅的过程

结合 then部分说到的 resolvePromise方法来实现最终的效果(注意,前面说过如果抛出异常则 promise2必须拒绝执行,所以需要使用 try..catch来完成这部分的实现):

class Promise {
  constructor(executor) {
    if (isUndef(executor) || !isFunction(executor)) {
      throw new TypeError('Promise resolver undefined is not a function')
    }
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = []  // 成功回调函数
    this.onRejectedCallbacks = []  // 失败回调函数
    const resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULLFILLED
        this.value = value
        // 发布
        this.onResolvedCallbacks.forEach(fn => fn())
      }
    }
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECT
        this.reason = reason
        this.onRejectedCallbacks.forEach(fn => fn())
      }
    }
    
    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }
}

Promise.prototype.then = function (onResolved, obRejected) {
  onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : data => data
  onReject = typeof onReject === 'function' ? onReject : err => { throw err; }
  const promise = new Promise((resolve, reject) => {
    if (this.status === FULLFILLED) {
      setTimeout(() => {
        try {
          const x = onResolved(this.value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (err) {
          reject(err)
        }
        
      }, 0)
    }
    if (this.status === REJECT) {
      setTimeout(() => {
        try {
          const x = obRejected(this.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (err) {
          reject(err)
        }
      }, 0)
    }
    if (this.status === PENDING) {
      this.onRejectedCallbacks.push(() => {
        setTimeout(() => {
        try {
          const x = obRejected(this.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (err) {
          reject(err)
        }
      }, 0)
      })
      this.onResolvedCallbacks.push(() => {
        setTimeout(() => {
          try {
            const x = onResolved(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (err) {
            reject(err)
          }
        }, 0)
      })
    }
  })
  return promise
}

宏任务和微任务

因为事件循环机制的存在,不同的异步任务被分为两类:宏任务和微任务。 而在我们的 Promise 中:

  • 宏任务:setTimeout——进入到宏任务的队列中
  • 微任务:.then——进入到微任务的队列中

那么对于宏任务和微任务是怎么执行的呢?

如果当前的执行栈已经执行完同步的任务,那么主线程就会先查看微任务队列,如果队列中有任务则会执行队列中的任务直至清空队列;如果不存在,就会去查看宏任务队列,将其里面的事件按照顺序依次加入当前的执行栈中。这是一个循环往复的过程(事件循环 Event Loop)。

源码

const PENGDING = 'PENDING'
const FULLFILLED = 'FULLFILLED'
const REJECT = 'REJECT'

// 定义该方法名为:resolvePromise
/**
 * 根据 x 和 promise2,使用 promise2 里的 resolve 和 reject 完成 then 调用之后的逻辑
 * @param {Promise} promise2 promise1.then 返回的新 Promise 对象
 * @param {[type]} x promise1 中 onFullfilled 或者 onReject 的返回值
 * @param {[type]} resolve promise2 中的 resolve 方法 
 * @param {[type]} reject promise2 中的 reject 方法
 */
 function resolvePromise(
  promise2,
  x,
  resolve,
  reject
) {
  let called;
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  if (
    (typeof x === 'object' && x !== null) ||
    (typeof x === 'function')
  ) {
    try {
      const then = x.then;
      if (typeof then === 'function') {
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject)
          },
          r => {
            if (called) return;
            reject(r)
          }
        )
      } else {
        if (called) return;
        called = true;
        resolve(x)
      }
    } catch (error) {
      if (called) return;
      called = true
      reject(error)
    }
  } else {
    if (called) return
    called = true
    resolve(x);
  }
}

class Promise {
  constructor(executor) {
    this.status = PENGDING;
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    const resolve = (value) => {
      if (this.status === PENGDING) {
        this.status = FULLFILLED
        this.value = value
        this.onResolvedCallbacks.forEach(fn => fn())
      }
    }
    const reject = (reason) => {
      if (this.status === PENGDING) {
        this.status = REJECT
        this.reason = reason
        this.onRejectedCallbacks.forEach(fn => fn())
      }
    }

    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }
  static resolve (value) {
    if (value instanceof Promise) {
      return value
    }
  
    if (
      (value instanceof Object) &&
      (value.then) &&
      (typeof value.then === 'function')
    ) {
      return new Promise((resolve, reject) => {
        value.then(resolve, reject)
      })  
    }
  
    return new Promise(resolve => {
      resolve(value)
    })
  }
  static reject (reason) {
    return new Promise((resolve, reject) => {
      reject(reason)
    })
  }
  static all (values) {
    return new Promise((resolve, reject) => {
      const array = []
      let index = 0;
    
      if (!Array.isArray(values)) {
        reject(new TypeError('Argument is not iterable'))
      }
    
      if (values.length === 0) {
        return resolve(values)
      }
    
      function processData(key, data) {
        array[key] = data
        if (++index === values.length) {
          resolve(array)
        }
      }
    
      values.forEach((item, index) => {
        Promise.resolve(item).then(data => {
          processData(index, data)
        }, reject)
      })
    })
}
  static race (values) {
    return new Promise((resolve, reject) => {
      if (!Array.isArray(values)) {
        reject(new TypeError('Argument is not iterable'))
      }
    
      if (values.length > 0) {
        values.forEach(item => {
          Promise.resolve(item).then(resolve, reject)
        })
      }
    })
  }
}

Promise.prototype.then = function (onFullfilled, onReject) {
  onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : data => data
  onReject = typeof onReject === 'function' ? onReject : err => { throw err; }
  
  const promise2 = new Promise((resolve, reject) => {
    if (this.status === FULLFILLED) {
      setTimeout(() => {
        try {
          const x = onFullfilled(this.value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (error) {
          reject(error)
        }
      }, 0)
    }
    if (this.status === REJECT) {
      setTimeout(() => {
        try {
          const x = onReject(this.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (error) {
          reject(error)
        }
      }, 0)
    }
    if (this.status === PENGDING) {
      this.onRejectedCallbacks.push(() => {
        setTimeout(() => {
          try {
            const x = onReject(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0)
      })
      this.onResolvedCallbacks.push(() => {
        setTimeout(() => {
          try {
            const x = onFullfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0)
      })
    }
  })
  return promise2
}

Promise.prototype.catch = function (onReject) {
  return this.then(null, onReject)
}

Promise.prototype.finally = function (cb) {
  return this.then(cb, cb)
}

Promise.deferred = function () {
  let dfd = {}
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  
  return dfd
}

module.exports = Promise

跑测试

要测试我们自己的 Promise是否符合 Promises/A+规范,需要使用官方提供的测试工具 promises-aplus-tests来进行测试。除此之外,还需要实现一个 deferred的静态方法,否则测试也跑不起来。

deferred

Promise.deferred = function () {
  let dfd = {}
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  
  return dfd
}

安装测试工具

npm install promises-aplus-tests -D

然后在项目目录中执行 promises-alpus-tests promise.js进行测试。当然我们也可以将命令直接配置到 package.json中去。

跑一下:

就这样,测试跑通。

Promise 的 any 方法和 allSettled 没写,后面再说吧