概述

Promisejs 异步编程的一种解决方案,避免了 “回调地狱” 给编程带来的麻烦,在 ES6 中成为了标准,这篇文章重点不是叙述 Promise 的基本用法,而是从零开始,手写一版符合 Promise/A+ 规范的 Promise,如果想了解更多 Promise 的基本用法,可以看 异步发展流程 —— Promise 的基本使用 这篇文章。

Promise 构造函数的实现

我们在使用 Promise 的时候其实是使用 new 关键字创建了一个 Promise 的实例,其实 Promise 是一个类,即构造函数,下面来实现 Promise 构造函数。

Promise/A+ 规范的内容比较多,详情查看 https://promisesaplus.com/我们在实现 Promise 逻辑时会根据实现的部分介绍相关的 Promise/A+ 规范内容。

Promise/A+ 规范中规定:

  • 构造函数的参数为一个名为 executor 的执行器,即函数,在创建实例时该函数内部逻辑为同步,即立即执行;
  • executor 执行时的参数分别为 resolvereject,一个为成功时执行的函数,一个为失败时执行的函数;
  • executor 执行时,一旦出现错误立即调用 reject 函数,并设置错误信息给 reason 属性;
  • 每个 Promise 实例有三个状态 pendingfulfilledrejected,默认状态为 pending
  • 状态只能从 pendingfulfilled 或从 pendingrejected,不可逆;
  • 执行 resolve 函数会使状态从 pending 变化到 fulfilled 并将参数存入实例的 value 属性中;
  • 执行 reject 函数会使状态从 pending 变化到 rejected 并将错误信息存入实例的 reason 属性中。

针对上面的 Promise/A+ 规范,Promise 构造函数代码实现如下:

/* promise.js -- Promise 构造函数 */
function Promise(executor) {
  var self = this;
  self.status = 'pending'; // 当前 Promise 实例的状态
  self.value = undefined; // 当前 Promise 实例成功状态下的值
  self.reason = undefined; // 当前 Promise 实例失败状态的错误信息
  self.onFulfilledCallbacks = []; // 存储成功的回调函数的数组
  self.onRejectedCallbacks = []; // 存储失败的回调函数的数组

  // 成功的执行的函数
  function resolve(value) {
    // 处理 executor 执行时,调用 resolve 将一个新的 Promise 实例作为结果返回
    if (value instanceof Promise) {
      return value.then(function (data) {
        resolve(data);
      }, function (err) {
        reject(err);
      });
    }

    if (self.status === 'pending') {
      self.status = 'fulfilled';
      self.value = value;

      // 每次调用 resolve 时
      // 执行 onFulfilledCallbacks 内部存储的所有的函数
      // 在实现 then 方法中详细说明
      self.onFulfilledCallbacks.forEach(function (fn) {
        fn();
      });
    }
  }

  // 失败执行的函数
  function reject(reason) {
    if (self.status === 'pending') {
      self.status = 'rejected';
      self.reason = reason;

      // 每次调用 reject 时
      // 执行 onRejectedCallbacks 内部存储的所有的函数
      // 在实现 then 方法中详细说明
      self.onRejectedCallbacks.forEach(function (fn) {
        fn();
      });
    }
  }

  // 调用执行器函数
  try {
    executor(resolve, reject);
  } catch (e) {
    // 如果执行器执行时出现错误,直接调用失败的函数
    reject(e);
  }
}

// 将自己的 Promise 导出
module.exports = Promise;

上面构造函数中的 resolvereject 方法在执行的时候都进行了当前状态的判断,只有状态为 pending 时,才能执行判断内部逻辑,当两个函数有一个执行后,此时状态发生变化,再执行另一个函数时就不会通过判断条件,即不会执行判断内部的逻辑,从而实现了两个函数只有一个执行判断内部逻辑的效果,使用如下:

/* verify-promise.js -- 验证 promise.js 的代码 */
// 引入自己的 Promise 模块
// 因为都验证代码都写在 verify-promise.js 文件中,后面就不再引入了
const Promise = require('./promise.js');

const p = new Promise((resolve, reject) => {
  // ...同步代码
  resolve();
  reject();
  // 上面两个函数只有先执行的 resolve 生效
});

实例方法的实现

then 方法的实现

没有 Promise 之前在一个异步操作的回调函数中返回一个结果在输入给下一个异步操作,下一个异步操作结束后需要继续执行回调,就形成回调函数的嵌套,在 Promise 中,原来回调函数中的逻辑只需要调用当前 Promise 实例的 then 方法,并在 then 方法的回调中执行,改变了原本异步的书写方式。

在 then 方法中涉及到的 Promise/A+ 规范:

  • Promise 实例的 then 方法中有两个参数,都为函数,第一个参数为成功的回调 onFulfilled,第二个参数为失败的回调 onRejected
  • Promise 内部执行 resolve 时,调用实例的 then 方法执行成功的回调 onFulfilled,当 Promise 内部执行 reject 或执行出错时,调用实例的 then 方法执行错误的回调 onRejected
  • then 方法需要支持异步,即如果 resovlereject 执行为异步时,then 方法的回调 onFulfilledonRejected 需要在后面执行;
  • Promise 需要支持链式调用,Promise 实例调用 then 方法后需要返回一个新的 Promise 实例。如果 then 的回调中有返回值且是一个 Promise 实例,则该 Promise 实例执行后成功或失败的结果传递给下一个 Promise 实例的 then 方法 onFulfilled (成功的回调)或 onRejected(失败的回调)的参数,如果返回值不是 Promise 实例,直接将这个值传递给下一个 Promise 实例 then 方法回调的参数,then 的回调如果没有返回值相当于返回 undefined
  • Promise 实例链式调用 then 时,当任何一个 then 执行出错,链式调用下一个 then 时会执行错误的回调,错误的回调没有返回值相当于返回了 undefined,再次链式调用 then 时会执行成功的回调;
  • Promise 实例的链式调用支持值的穿透,即当上一个 then 没有传递回调函数,或参数不是一个函数时(null),参数要被忽略,需要后面调用的 then 的回调函数来接收之前执行成功或失败的结果;
  • executorPromise 构造函数中执行时使用 try...catch... 捕获异常,但是内部执行的代码有可能是异步的,所以需要在 then 方法中使用 try...catch... 再次捕获;
  • Promise 实例的 then 方法中的回调为 micro-tasks(微任务),回调内的代码应晚于同步代码执行,在浏览器内部调用微任务接口,我们这里模拟使用宏任务代替。

针对上面的 Promise/A+ 规范,then 方法代码实现如下:

/* promise.js -- then 方法 */
Promise.prototype.then = function (onFulfilled, onRejected) {
  // 实现值的穿透
  if (typeof onFulfilled !== 'function') {
    onFulfilled = function (data) {
      return data;
    }
  }

  if (typeof onRejected !== 'function') {
    onRejected = function (err) {
      throw err;
    }
  }

  var self = this;

  // 返回新的 Promise,规范中规定这个 Promise 实例叫 promise2
  var promise2 = new Promise(function (resolve, reject) {
    if (self.status === 'fulfilled') {
      // 用宏任务替代模拟微任务,目的是使 `then` 的回调晚于同步代码执行
      // 并保证执行 resolvePromise 时可以获取到 promise2 作为参数
      setTimeout(function () {
        try {  // 捕获异步的异常
          // onFulfilled 执行完返回值的处理,x 为成功回调的返回值
          var x = onFulfilled(self.value);

          // 处理返回值单独封装一个方法
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      }, 0);
    }

    if (self.status === 'rejected') {
      setTimeout(function () {
        try {
          // onRejected 执行完返回值的处理,x 为失败回调的返回值
          var x = onRejected(self.reason);
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      }, 0);
    }

    // 如果在 Promise 执行 resolve 或 renject 为异步
    // 将 then 的执行程序存储在实例对应的
    // onFulfilledCallbacks 或 onRejectedCallbacks 中
    if (self.status === 'pending') {
      self.onFulfilledCallbacks.push(function () {
        setTimeout(function () {
          try {
            var x = onFulfilled(self.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      });

      self.onRejectedCallbacks.push(function () {
        setTimeout(function () {
          try {
            var x = onRejected(self.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      });
    }
  });

  return promise2;
};

在处理 then 回调的返回值时,其实就是在处理该返回值与 then 方法在执行后返回的新 Promise 实例(即 promise2)之间的关系,因为无论 Promise 的执行器在执行 resolve 还是 reject 是同步或是异步,都需要进行处理,所以我们单独封装一个函数 resolvePromise 来处理。

resolvePromise 函数有四个参数:

  • promise2then 执行后返回的 Promise 实例;
  • xthen 的回调返回的结果;
  • resolvepromise2resolve 函数;
  • rejectpromise2reject 函数。

resolvePromise 函数中涉及到的 Promise/A+ 规范:

  • 将每个 Promise 实例调用 then 后返回的新 Promise 实例称为 promise2,将 then 回调返回的值称为 x
  • 如果 promise2x 为同一个对象,由于 x 要将执行成功或失败的结果传递 promise2then 方法回调的参数,因为是同一个 Promise 实例,此时既不能成功也不能失败(自己不能等待自己完成),造成循环引用,这种情况下规定应该抛出一个类型错误来回绝;
  • 如果 x 是一个对象或者函数且不是 null,就去取 xthen 方法,如果 x 是对象,防止 x 是通过 Object.defineProperty 添加 then 属性,并添加 getset 监听,如果在监听中抛出异常,需要被捕获到,x.then 是一个函数,就当作 x 是一个 Promise 实例,直接执行xthen 方法,执行成功就让 promise2 成功,执行失败就让 promise2 失败,如果 x.then 不是函数,则说明 x 为普通值,直接调用 promise2resolve 方法将 x 传入,不满足条件说明该返回值就是一个普通值,直接执行 promise2resolve 并将 x 作为参数传入;
  • 如果每次执行 xthen 方法,回调中传入的参数还是一个 Promise 实例,循环往复,需要递归 resolvePromise 进行解析;
  • 在递归的过程中如果返回的 Promise 是其他人通过 Promise/A+ 规范实现的,并且实现 Promisethen 方法中同时调用了 resolvereject 的情况(未严格按照规范,有坑的 Promise),应该声明一个标识变量 called 做判断来避免这种情况,第一次调用 resolvereject 生效,然后更改标识来忽略其他的调用。

针对上面的 Promise/A+ 规范,resolvePromise 函数代码实现如下:

/* promise.js -- resolvePromise 方法 */
function resolvePromise(promise2, x, resolve, reject) {
  // 判断 x 和 promise2 是不是同一个函数
  if (promise2 === x) {
    reject(new TypeError('循环引用'));
  }

  // x 是对象或者函数并且不是 null,如果不满足该条件说明 x 只是一个普通的值
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    // 标识变量,防止递归内外层 resolve 和 reject 同时调用
    // 针对 Promise,x 为普通值的时候可以放行
    var called;

    // 为了捕获 Object.defineProperty 创建的 then 属性时添加监听所抛出的异常
    try {
      // then 只取一次,防止 Object.defineProperty getter 内,用次数控制报错
      var then = x.then;

      // then 为一个方法,就当作 x 为一个 promise
      if (typeof then === 'function') {
        // 执行 then,第一个参数为 this(即 x)
        // 第二个参数为成功的回调,第三个参数为失败的回调
        then.call(x, function (y) {
          if (called) return;
          called = true;

          // 如果 y 是 Promise 就继续递归解析
          resolvePromise(promise2, y, resolve, reject);
        }, function (err) {
          if (called) return;
          called = true;
          reject(err);
        });
      } else { // x 是一个普通对象,直接成功即可
        resolve(x);
      }
    } catch(e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

注意:Promise 是一套规范,是通用的,每个人都可以实现一个符合 Promise/A+ 规范的 Promise,其他人实现的 Promise 可能会在我们的 Promisethen 方法中被返回,所以当我们取出这个对象查看是否含有 then 方法,如果有则认为这个对象是一个 Promise 的实例。

上面我们按照 Promise/A+ 规范实现了 Promisethen 方法,接下来针对上面的规范,用一些有针对行的案例来对 then 方法一一进行验证。

验证异步调用 resolvereject

/* 文件:verify-promise.js */
// 验证 promise.js 异步调用 resolve 或 reject
const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve(), 1000);
});

p.then(() => console.log('执行了'));

// 执行了

验证链式调用 then 返回 Promise 实例:

/* 文件:verify-promise.js */
// 验证 promise.js then 回调返回 Promise 实例
const p1 = new Promise((resolve, reject) => resolve());
const p2 = new Promise((resolve, reject) => resolve('hello'));

p1.then(() => p2).then(data => console.log(data));

// hello

验证链式调用 then 返回普通值:

/* 文件:verify-promise.js */
// 验证 promise.js then 回调返回普通值
const p = new Promise((resolve, reject) => resolve());

p.then(() => 'hello').then(data => console.log(data));

// hello

验证链式调用 then 中执行出错链式调用 then 执行错误的回调后,再次链式调用 then

/* 文件:verify-promise.js */
// 验证 promise.js 链式调用 then 中执行出错
// 链式调用 then 执行错误的回调后,再次链式调用 then
const p = new Promise((resolve, reject) => resolve());

p.then(() => {
  throw new Error('error');
}).then(() => {
  console.log('success');
}, err => {
  console.log(err);
}).then(() => {
  console.log('成功');
}, () => {
  console.log('失败');
});

// Error: error  at p.then...
// 成功

验证 then 的参数穿透:

/* 文件:verify-promise.js */
// 验证 then 的参数穿透
const p1 = new Promise((resolve, reject) => resolve('ok'));

const p2 = p1.then().then(data => {
  console.log(data);
  throw new Error('出错了');
});

p2.then().then(null, err => console.log(err));

// ok
// 出错了

验证 then 方法是否晚于同步代码执行:

/* 文件:verify-promise.js */
// 验证 then 方法是否晚于同步代码执行
const p = new Promise((resolve, reject) => {
  resolve(1);
});

p.then(data => console.log(data));
console.log(2);

// 2
// 1

验证循环引用:

/* 文件:verify-promise.js */
// 验证 promise.js 循环引用
const p1 = new Promise((resolve, reject) => resolve());

// 让 p1 then 方法的回调返回自己
var p2 = p1.then(() => {
  return p2;
});

p2.then(() => {
  console.log('成功');
}, err => {
  console.log(err);
});

// TypeError: 循环引用  at resolvePromise...

验证 then 回调返回对象通过 Object.definePropertype 添加 then 属性并添加 get 监听,在触发监听时抛出异常:

/* 文件:verify-promise.js */
// 验证 promise.js then 回调
// 返回对象通过 Object.definePropertype 添加 then 和 get 监听,捕获异常
let obj = {};
let index = 0;
Object.defineProperty(obj, 'then', {
  get () {
    index++;
    if (index === 2) {
      throw new Error();
    }
  }
});

let p = new Promise((resolve, reject) => resolve());
p.then(() => {
  return obj;
}).then(() => {
  console.log('成功');
}, () => {
  console.log('出错了');
});

// 出错了

验证每次执行 resolve 都传入 Promise 实例,需要将最终的执行结果传递给下一个 Promise 实例 then 的回调中:

/* 文件:verify-promise.js */
// 验证 promise.js 每次执行 resolve 都传入 Promise 实例
const p = new Promise((resolve, reject) => resolve());

p.then(() => {
  return new Promise((resolve, reject) => {
    resolve(new Promise(resolve, reject) => {
      resolve(new Promise(resolve, reject) => {
        resolve(200);
      });
    });
  });
}).then(data => {
  console.log(data);
});

// 200

catch 方法的实现

/* promise.js -- catch 方法 */
Promise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected);
}

catch 方法可以理解为是 then 方法的一个简写,只是参数中少了成功的回调,所以利用 Promise/A+ 规范中参数穿透的特性,很容易就实现了 catch 方法,catch 方法的真相就是这么的简单。

验证 catch 方法:

/* 文件:verify-promise.js */
// 验证 promise.js 的 catch 方法
const p = new Promise((resolve, reject) => reject('err'));

p.then().catch(err => {
  console.log(err);
}).then(() => {
  console.log('成功了');
});

// err
// 成功了

finally 方法的实现

/* promise.js -- finally 方法 */
Promise.prototype.finally = function (callback) {
  return this.then(
    value => Promise.resolve(callback()).then(() => value),
    reason => Promise.resolve(callback()).then(() => { throw reason })
  );
}

finally 方法是在 ES9 的规范中实现的,特点是无论成功或者失败都会执行传入的回调函数,并且回调函数没有任何参数,而在 finally 后可以再链式调用 thencatch 去处理后续逻辑或捕获异常,因为参数不是在 finally 方法传入的回调中返回的,所以借助了静态方法 Promise.resolve(该方法实现方式在静态方法中会说明),在 finally 内部调用 then 返回 Promise 实例,并传入成功和失败的回调用来返回最初成功或失败的结果,finally 的回调中执行的返回结果不生效。

验证 finally 方法:

/* 文件:verify-promise.js */
// 验证 promise.js 的 finally 方法
const p = new Promise((resolve, reject) => resolve('nihao'));

p.finally(() => { // node版本的问题 8以上
  console.log('hello');
  throw 'error'
}).catch(err => {
  console.log('err', err);
});

p.finally(() => {
  console.log('hello');
  return 100;
}).then(data => {
  console.log('data', data);
});

// hello
// hello
// err error
// data nihao

静态方法的实现

Promise.resolve 方法的实现

Promise.resolve 方法传入一个参数,并返回一个新的 Promise 实例,这个参数作为新 Promise 实例 then 方法成功回调的参数,在调用时感觉直接成功了,其实是直接执行了返回 Promise 实例的 resolve

/* promise.js -- Promise.resolve 方法 */
Promise.resolve = function (val) {
  return new Promise(function (resolve, reject) {
    resolve(val);
  });
}

验证 Promise.resolve 方法:

/* 文件:verify-promise.js */
// 验证 promise.js 的 Promise.resolve 方法
Promise.resolve('成功了').then(data => console.log(data));

// 成功了

Promise.reject 方法的实现

Promise.reject 方法与 Promise.resolve 的实现思路相同,不同的是,直接调用了返回新 Promise 实例的 reject

/* promise.js -- Promise.reject 方法 */
Promise.reject = function (reason) {
  return new Promise(function (resolve, reject) {
    reject(reason);
  });
}

验证 Promise.reject 方法:

/* 文件:verify-promise.js */
// 验证 promise.js 的 Promise.reject 方法
Promise.reject('失败了').then(err => console.log(err));

// 失败了

Promise.all 方法的实现

Promise.all 方法可以实现多个 Promise 实例的并行,返回值为一个新的 Promise 实例,当所有结果都为成功时,返回一个数组,该数组存储的为每一个 Promise 实例的返回结果,这些 Promise 实例的返回顺序先后不确定,但是返回值的数组内存储的返回结果是按照数组中 Promise 实例最初顺序进行排列的,返回的数组作为返回 Promise 实例成功回调的参数,当其中一个失败,直接返回失败实例的错误信息,并作为返回 Promise 实例失败回调的参数。

/* promise.js -- Promise.all 方法 */
Promise.all = function (promises) {
  return new Promise(function (resolve, reject) {
    // 存储返回值
    var result = [];

    // 代表存入的个数,因为 Promise 为异步
    // 不知道哪个 Promise 先成功,不能用数组的长度来判断
    var idx = 0;

    // 用来构建全部成功的返回值
    function processData(index, data) {
      result[index] = data; // 将返回值存入数组
      idx++;

      if (idx === promises.length) {
        resolve(result);
      }
    }

    for (var i = 0; i < promises.length; i++) {
      var current = promises[i];
      // 因为 Promise 为异步,保证 i 值是顺序传入
      (function (i) {
        if (current && current.then && typeof current.then === 'function') {
          current.then(function (data) {
            processData(i, data);
          }, reject);
        } else {
          processData(i, current);
        }
      })(i);
    }
  });
}

验证 Promise.all 方法:

/* 文件:verify-promise.js */
// 验证 promise.js 的 Promise.all 方法
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 2000);
});
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve(2), 1000);
});

Promise.all([p1, p2, 3]).then(data => console.log(data));

// [1, 2, 3]

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 2000);
});
const p4 = new Promise((resolve, reject) => {
  setTimeout(() => reject(2), 1000)
});

Promise.all([p3, p4]).then(data => {
  console.log(data);
}).catch(err => {
  console.log(err);
});

// 2

Promise.race 方法的实现

Promise.race 方法与 Promise.all 类似,同样可以实现多个 Promise 实例的并行,同样返回值为一个新的 Promise 实例,参数同样为一个存储多个 Promise 实例的数组,区别是只要有一个 Promise 实例返回结果,无论成功或失败,则直接返回这个结果,并作为新 Promise 实例 then 方法中成功或失败的回调函数的参数。

/* promise.js -- Promise.race 方法 */
Promise.race = function (promises) {
  return new Promise(function (resolve, reject) {
    for (var i = 0; i < promises.length; i++) {
      var current = promises[i];

      if (current && current.then && typeof current.then === 'function') {
        current.then(resolve, reject);
      } else {
        resolve(current);
      }
    }
  });
}

验证 Promise.race 方法:

/* 文件:verify-promise.js */
// 验证 promise.js 的 Promise.race 方法
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 2000);
});
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve(2), 1000);
});

Promise.race([p1, p2]).then(data => console.log(data));

// 2

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 2000);
});
const p4 = new Promise((resolve, reject) => {
  setTimeout(() => reject(2), 1000);
});

Promise.all([p3, p4]).then(data => {
  console.log(data);
}).catch(err => {
  console.log(err);
});

// 2

测试 Promise 是否符合 Promise/A+ 规范

promises-aplus-test 是专门用来验证 Promise 代码(构造函数和 then 方法)是否符合 Promise/A+ 规范的模块,需要通过 npm 下载。

$ npm install promises-aplus-test -g

测试方法:

  • promise.js 中写入测试代码;
  • 在命令行中输入命令 promises-aplus-test + fileName

测试代码:

/* promise.js -- 测试方法 Promise.derfer */
// Promise 语法糖
// 好处:解决 Promise 嵌套问题
// 坏处:错误处理不方便
Promise.derfer = Promise.deferred = function () {
  const dfd = {};

  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve;
    dfd.reject = reject;
  });

  return dfd;
}

输入命令:

$ promises-aplus-test promise.js

执行上面命令后,会根据 Promise/A+ 规范一条一条进行极端的验证,当验证通过后会在窗口中这一条对应的执行项前打勾,验证不通过打叉,直到所有的规范都验证完毕。