手写系列-这一次,彻底搞懂 Promise

3,342 阅读9分钟

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

一、前言

想要实现 Promise,必须先了解 Promise 是什么,以及 Promise 有哪些功能。

还不是特别了解 Promise 的同学,建议先移步 ES6入门-Promise 熟悉。

Promise 是基于 Promises/A+ 规范 实现的,换句话说,我们可以按照 Promises/A+ 规范 来手写 Promise。

1.1 小例子

Promise,直译过来就是承诺,Promise 到底承诺了什么呢?

当我在麦当劳点一份汉堡套餐,收银员会给我一张收据,这个收据就是 Promise,代表我已经付过钱了,麦当劳会为我做一个汉堡套餐的承诺,我要通过收据来取这个汉堡套餐。

那么这个买汉堡得到的承诺会有以下 3 种状态:

  1. 等待状态:我刚下单,汉堡还没做好,这时我可以在等待汉堡时,同时做其他事情;
  2. 成功状态:汉堡做好了,通知我取餐;
  3. 失败状态:发现卖完了,通知我退款;

需要注意的是,状态的修改是不可逆的,当汉堡做好了,承诺兑现了,就不能再回到等待状态了。

总结一下,Promise 就是一个承诺,承诺会给你一个处理结果,可能是成功的,可能是失败的,而返回结果之前,你可以同时做其他事情。

二、Promises/A+

接下来,按照 Promises/A+ 规范 一步步实现 Promise。

1. Promise 基本用法

先瞅一眼 ES6 Promise 基本用法。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(data => {
    console.log('请求成功')
}, err => {
    console.log('请求失败')
})

1.1 Promise 状态

Promise 拥有自己的状态,初始状态->成功状态时,执行成功回调,初始状态->失败状态时,执行失败回调。

  1. pending:初始状态,可以转换为 fulfilled 或 rejected 状态;
  2. fulfilled:成功状态,转换到该状态时必须有成功返回值,且不能再次转换状态;
  3. rejected:失败状态,转换到该状态时必须有错误原因,且不能再次转换状态;

通过已知的 Promise 3 种状态,可定义常量 STATUS 和 MyPromise 状态 status。

代码如下:

// Promise 3 种状态
const STATUS = {
  PENDING: 'pending',
  FULFILLED: 'fulfilled',
  REJECTED: 'rejected'
}

class MyPromise {
    // 初始状态为 pending
    status = STATUS.PENDING
}

1.2 执行器

从基本用法可知,Promise 需要接收 1 个执行器函数作为参数,这个函数带有 2 个参数。

  1. resolve:把 Promise 状态改成成功;
  2. reject:把 Promise 状态改成失败;

代码如下:

class MyPromise {
  constructor (executor) {
      // 执行器
      executor(this.resolve, this.reject)
  }
  // 成功返回值
  value = null
  
  // 失败返回值
  reason = null
  
  // 修改 Promise 状态,并定义成功返回值
  resolve = value => {
      if (this.status === STATUS.PENDING) {
          this.status = STATUS.FULFILLED
          this.value = value
      }
  }
  
  // 修改 Promise 状态,并定义失败返回值
  reject = () => {
      if (this.status === STATUS.PENDING) {
              this.status = STATUS.REJECTED
              this.reason = value
          }
      }
}
}

1.3 then

Promise 拥有 then 方法,then 方法第一个参数是成功状态的回调函数 onFulfilled,第二个参数是失败状态的回调函数 onRejected。

promise.then(onFulfilled, onRejected)

onFulfilled 要求如下:

  • 必须在 promise 状态为完成时调用它,并将 promise 的 value 作为它的第一个参数;
  • 在 promise 完成之前不能调用它;
  • 它不能被多次调用;

onRejected 要求如下:

  • 必须在 promise 被拒绝后调用它,以 promise.reason 作为它的第一个参数;
  • 在 promise 被拒绝之前不能调用它;
  • 它不能被多次调用;

代码如下:

class MyPromise {
    then = function (onFulfilled, onRejected) {
      if (this.status === STATUS.FULFILLED) {
          onFulfilled(this.value)
      } else if (this.status === STATUS.REJECTED) {
          onRejected(this.reason)
      }
    }
}

1.4 试试看

按照 Promise 的基本用法,创建 MyPromise 实例 mypromise。

const mypromise = new MyPromise((resolve, reject) => {
  resolve('成功')
})

mypromise.then(data => {
  console.log(data, '请求成功') // 成功打印“成功 请求成功”
}, err => {
  console.log(err, '请求失败')
})

试行成功,打印结果为“成功 请求成功”。

源码地址:基本用法源码

2. Promise.then

下文将按照 Promises/A+ 规范 完善 MyPromise.then 方法。

Promises/A+ 规范 中标明 then 有以下要求:

1. 可选参数

onFulfilled、onRejected 是可选参数。

  • 如果 onFulfilled 不是函数,则必须忽略它;
  • 如果 onRejected 不是函数,则必须忽略它;

代码如下:

class MyPromise {
    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
        onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }
    }
}

2. 多次调用 then

then 可以在同一个承诺上多次调用。

  • 当 promise 完成,所有相应的 onFulfilled 回调必须按照它们的原始调用的顺序执行 then;
  • 当 promise 被拒绝,所有相应的 onRejected 回调必须按照它们对 的原始调用的顺序执行 then;

2.1 数组缓存回调

可以理解为将 onFulfilled、onRejected 作为数组存储在 MyPromise 中,然后按照顺序执行。

代码如下:

class MyPromise {
  // 成功回调
  onFulfilledCallback = []

  // 失败回调
  onRejectedCallback = []

  // 修改 Promise 状态,并定义成功返回值
  resolve = value => {
    if (this.status === STATUS.PENDING) {
        this.status = STATUS.FULFILLED
        this.value = value

        while(this.onFulfilledCallback.length) {
            this.onFulfilledCallback.shift()(value)
        }
    }
  }
  
  // 修改 Promise 状态,并定义失败返回值
    reject = value => {
        if (this.status === STATUS.PENDING) {
            this.status = STATUS.REJECTED
            this.reason = value

            while(this.onRejectedCallback.length) {
                this.onRejectedCallback.shift()(value)
            }
        }
    }

    then = function (onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
        onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
        if (this.status === STATUS.PENDING) {
          this.onFulfilledCallback.push(onFulfilled)
          this.onRejectedCallback.push(onRejected)
        } else if (this.status === STATUS.FULFILLED) {
            onFulfilled(this.value)
        } else if (this.status === STATUS.REJECTED) {
            onRejected(this.reason)
        }
    }
}

由此,我们已实现了一个基础的 Promise。

2.2 试试看

看了这么久,试一试 MyPromise 是否符合要求吧。

代码如下:

const mypromise = new MyPromise((resolve, reject) => {
  resolve('成功')
})

mypromise.then(data => {
  console.log(data, '1')
})

mypromise.then(data => {
  console.log(data, '2')
})

输出结果如图:

image.png

由图可知,和预期一样。

源码地址:多次调用then 源码

3. 链式调用 then

then 必须返回一个 Promise 来支持链式调用 Promise。

示例代码如下:

mypromise.then(data => {
  console.log(data, '请求成功')
  return '2'
}).then(data => {
  console.log(data, '请求成功')
  return '3'
})

3.1 改写 then 方法

改动点如下:

  • then 方法需要返回 MyPromise 实例;
  • then 内部调用回调时,需通过 resolvePromise 方法判断返回值 x 的类型来处理返回值。
class MyPromise {
    then = function (onFulfilled, onRejected) {
        // 返回 MyPromise实例
      const promise2 = new MyPromise((resolve, reject) => {
        if (this.status === STATUS.PENDING) {
            this.onFulfilledCallback.push(() => {
                const x = onFulfilled(this.value)
                resolvePromise(promise2, x, resolve, reject)
            })
            this.onRejectedCallback.push(() => {
                const x = onRejected(this.value)
                resolvePromise(promise2, x, resolve, reject)
            })
        } else if (this.status === STATUS.FULFILLED) {
            const x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
        } else if (this.status === STATUS.REJECTED) {
            const x = onRejected(this.error)
            resolvePromise(promise2, x, resolve, reject)
        }
      }) 

      return promise2
    }
}

上述代码引用了 resolvePromise 来处理 Promise.then 的返回值,

3.2 resolvePromise

Promises/A+ 规范 对resolvePromise 的要求如下:

image.png

  • 如果 promise2 === x, 执行 reject,错误原因为 TypeError
  • 如果 x 是函数或对象
    • 如果 x.then 是函数
      • 执行 x.then
    • 如果 x.then 不是函数
      • 执行 resolve(x)
  • 如果 x 不是函数或对象
    • 执行 resolve(x)

代码如下:

function resolvePromise (promise2, x, resolve, reject) {
  // 如果 promise2 === x, 执行 reject,错误原因为 TypeError
    if (promise2 === x) {
      reject(new TypeError('The promise and the return value are the same'))
    }

    // 如果 x 是函数或对象
    if (typeof x === 'object' || typeof x === 'function') {
      let then
      try {
        then = x.then
      } catch (error) {
        reject(error)
      }

      // 如果 x.then 是函数
      if (typeof then === 'function') {
        then.call(x, y => {
          // resolve的结果依旧是promise 那就继续解析
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          reject(err);// 失败了
        })
      } else {
          // 如果 x.then 不是函数
          resolve(x)
      }
    } else {
        // 如果 x 不是 promise 实例
        resolve(x)
    }
}

3.3 试一试

试试看能不能符合预期,链式调用 then 吧。

const mypromise = new MyPromise((resolve, reject) => {
  resolve('成功')
})

const mypromise2 = new MyPromise((resolve, reject) => {
  resolve('成功2')
})

mypromise.then(data => {
  console.log(data, '1')
  return mypromise2 
}).then(data => {
  console.log(data, '2')
})

输出结果为:

image.png

成功符合预期!

源码地址:链式调用then源码

4. 异步事件

Promises/A+ 规范 要求 onFulfilled、onRejected 在执行上下文堆栈之前不得调用。

4.1 事件队列

当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为事件队列。

当所有同步任务执行完成后,系统才会读取"事件队列"。

事件队列中的事件分为宏任务和微任务:

  1. 宏任务:浏览器/Node发起的任务,如 window.setTimeout;
  2. 微任务:Js 自身发起的,如 Promise;

事件队列就是先执行微任务,再执行宏任务,而宏任务和微任务包含以下事件:

宏任务微任务
setTimeoutPromise
setIntervalqueueMicrotask
script(整体代码块)-

看看下面这个例子,你知道答案吗?

setTimeout(function () {
  console.log(1);
});
new Promise(function(resolve,reject){
  console.log(2)
  resolve(3)
}).then(function(val){
  console.log(val);
})
console.log(4);

打印结果的顺序是2->4->3->1。事件队列如下:

  1. 主队列,同步任务,new Promise 内部的同步任务
new Promise(function(resolve,reject){
  console.log(2)
  })
  1. 主队列,同步任务,new Promise 后的 console.log(4)
console.log(4)
  1. 异步任务的微任务
promise.then(function(val){
  console.log(val);
})
  1. 异步任务的宏任务
setTimeout(function () {
  console.log(1);
});

因此,想要实现 onFulfilled、onRejected 在执行上下文堆栈之前不得调用,我们需要把 onFulfilled、onRejected 改造成微任务,这里使用 queueMicrotask 来模拟实现微任务,代码如下:

class MyPromise {
     then (onFulfilled, onRejected) {
        const realOnFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
        const realOnRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }
      
        const promise2 = new MyPromise((resolve, reject) => {
          
          const fulfilledMicrotask = () =>  {
            // 创建一个微任务等待 promise2 完成初始化
            queueMicrotask(() => {
              try {
                // 获取成功回调函数的执行结果
                const x = realOnFulfilled(this.value);
                // 传入 resolvePromise 集中处理
                resolvePromise(promise2, x, resolve, reject);
              } catch (error) {
                reject(error)
              } 
            })  
          }
  
        const rejectedMicrotask = () => { 
            // 创建一个微任务等待 promise2 完成初始化
            queueMicrotask(() => {
              try {
                // 调用失败回调,并且把原因返回
                const x = realOnRejected(this.reason);
                // 传入 resolvePromise 集中处理
                resolvePromise(promise2, x, resolve, reject);
              } catch (error) {
                reject(error)
              } 
            }) 
          }

          if (this.status === STATUS.PENDING) {
              this.onFulfilledCallbacks.push(fulfilledMicrotask)
              this.onRejectedCallbacks.push(rejectedMicrotask)
          } else if (this.status === STATUS.FULFILLED) {
            fulfilledMicrotask()
          } else if (this.status === STATUS.REJECTED) {
            rejectedMicrotask()
          }
        }) 

        return promise2
    }
}

下面试试能不能成功?

4.2 试试看

const mypromise = new MyPromise((resolve, reject) => {
  setTimeout(() => resolve('成功'), 1000)
})

const mypromise2 = new MyPromise((resolve, reject) => {
  setTimeout(() => resolve('成功2'), 1000)
})

mypromise.then(data => {
  console.log(data, '1')
  return mypromise2 
}).then(data => {
  console.log(data, '2')
})

打印结果如图:

image.png

成功按顺序打印。

源码地址:异步事件源码

3. Promise/A+ 测试

下面将用 Promise/A+ 测试工具 promises-aplus-tests 测试我们手写的 Promise 是否符合规范。

3.1 安装 promises-aplus-tests

npm install promises-aplus-tests -D

3.2 为 MyPromise 添加 deferred

MyPromise {
  ......
}

MyPromise.deferred = function () {
  var result = {};
  result.promise = new MyPromise(function (resolve, reject) {
    result.resolve = resolve;
    result.reject = reject;
  });

  return result;
}
module.exports = MyPromise;

3.3 配置启动命令

"scripts": {
    "test:promise": "promises-aplus-tests ./src/手写系列/Promise/testPromise"
  },

3.4 开始测试

npm run test:promise

哇哦,全部成功!!

image.png

源码地址:testPromise.js 源码

三、总结

以上,我们实现了一个符合 Promises/A+ 规范 的 Promise,我们可以继续自己动手,参考 ES6 的 Promise 方法对 MyPromise 进行拓展练习。

总结一下 Promise 其实就是一个帮助我们执行异步任务的对象,因为 Javascript 单线程的特性,导致必须通过为异步任务添加回调来得到异步任务的结果。为了解决回调地狱,Promise 应运而生。

Promise 通过对异步任务执行状态的处理,让我们可以在 Promise.then 中获取任务结果,让代码更加清晰优雅。

Promise.then 的链式调用,以顺序的方式来表达异步流,让我们更好的维护异步代码。

可通过 github源码 进行实操练习。

希望能对你有所帮助,感谢阅读~别忘了点个赞鼓励一下我哦,笔芯❤️