实现一个玩具 Promise ~

hello~亲爱的观众老爷们大家好,有段时间没写文章了,最近主要忙于工作交接,实在没腾出时间进行总结。这次为大家带来 es6 中 Promise 的简单实现。

事实说类似的文章已经不少,不少大神对此都有精彩的实现。然而自己消化了才是最好的,在看文章中其实还是遇到不少坑,如 this 的指向,回调的到底是哪个 Promise 等。本文尽量解释清楚上述问题,力求让大家更好地掌握 Promise

发布/订阅 的简单实现

JS 的异步编程相信大家都比较熟悉,在 Promise 出现之前,主要是使用 发布/订阅 模式、回调函数等方式实现异步的。在我的理解中,其实 Promise 特别像 发布/订阅 是在使用 发布/订阅 模式,在合适的时机调用 resolve(),就如发布事件一样,订阅者的回调函数即被注册。因而我们先实现一个简单的 发布/订阅 模式:

function Promise(fn) {
  //订阅者回调函数数组
  this.callBacks = [];
  //发布信息 调用订阅者的回调函数 使用箭头函数可以保证this的指向是此Promise实例
  const resolve = val => {
    //执行回调
    this.callBacks.forEach(fn => {
      fn(val);
    })
  };
  //将resolve作为实参传入fn中,交由fn决定何时发布信息
  fn(resolve);
}

熟悉 发布/订阅 的童鞋,相信看这个代码会感觉特别熟悉,不太熟悉的童鞋可以思考这么个场景:去商城买衣服时,刚好没了你的码数,于是你留下的电话(推入 calBacks 数组中),于是销售小姐姐有货的时候通知我回来买(传入的 fn 调用 resolve, 发布信息)。典型的 好莱坞原则实现。

上述代码还缺少一个销售小姐姐记录电话,也就是将一些函数推入 callBacks 的实现,我们可以添加这样的代码:

Promise.prototype.then = function(successCallback) {
  this.callBacks.push(successCallback);
};

尝试一下玩一下:

const p = new Promise(function(res) {
  setTimeout(() => {
    res(123);
  }, 1000);
});

p.then(function(val) {
  console.log(val);
})

在浏览器执行上述代码后,可以看到1s后控制台中打印出 123

添加状态

上面的通过 发布/订阅 实现 Promise 虽然运行良好,然而还是缺了日常使用 Promise 中的状态:如果去买衣服的时候已经有我的码数了,那留电话让销售小姐姐再打电话我是毫无意义的,肯定是当场买走好了啊。因而我们引入状态,为了简单实现,暂时先引入没码数的状态: pending 与可以立即买走的状态: fulfill

首先修改 then 方法,如果有码数也就是 fulfill 时立即执行回调:

Promise.prototype.then = function(successCallback) {
  //如果promise已经决议 那么久立即执行回调
  if(this.status === 'fulfill'){
    successCallback();
  }else{
    this.callBacks.push(successCallback);
  }
};

但我们会发现,此时无法将 Promise 决议后的值传入 successCallback 中,这也好办,在构造函数中再定义一个 _val 记录 Promise 决议后的值就好,完整实现如下:

function Promise(fn) {
  //订阅者回调函数数组
  this.callBacks = [];
  //promise的状态 一开始时必然是pending
  this.status = 'pending';
  //记录决议后的值 一开始默认为null
  this._val = null;
  //发布信息 调用订阅者的回调函数 使用箭头函数可以保证this的指向是此Promise实例
  const resolve = val => {
    //切换状态
    this.status = 'fulfill';
    //决议赋值
    this._val = val;
    //执行回调
    this.callBacks.forEach(fn => {
      fn(val);
    })
  };
  //将resolve作为实参传入fn中,交由fn决定何时发布信息
  fn(resolve);
}

Promise.prototype.then = function(successCallback) {
  //如果promise已经决议 那么久立即执行回调 不然就先推入回调数组中
  if (this.status === 'fulfill') {
    successCallback(this._val);
  } else {
    this.callBacks.push(successCallback);
  }
};

测试一下:

const p = new Promise(function(res) {
  setTimeout(() => {
    res(123);
  }, 1000)
});
p.then(function(val) {
  console.log(val);
});

setTimeout(() => {
  p.then(function(val) {
    console.log(val);
  });
}, 2000)

控制台一秒后打印出 123 ,再隔一秒后再次打印出 123。So far so good~

链式调用

es6 中,Promise 是可以链式调用的,而现在还不行。一般而言,为了达成链式调用,最简单直接的方法就是在 then 调用后 return this;,然而在 Promise 的实现中,这是不折不扣的陷阱。试想一下,如果不断通过 return this; 来达到链式调用,那么该如何管理回调函数的数组呢?而根据Promise/A+规范,每次调用 then 都会返回一个新的 Promise。通过不断返回新的 Promise,回调函数的数组管理起来就很方便啦!按照这个思路实现看一下:

function Promise(fn) {
  //订阅者回调函数数组
  this.callBacks = [];
  //promise的状态 一开始时必然是pending
  this.status = 'pending';
  //记录决议后的值 一开始默认为null
  this._val = null;
  //发布信息 调用订阅者的回调函数 使用箭头函数可以保证this的指向是此Promise实例
  const resolve = val => {
    //切换状态
    this.status = 'fulfill';
    //决议赋值
    this._val = val;
    //执行回调
    this.callBacks.forEach(fn => {
      fn(val);
    })
  };
  //将resolve作为实参传入fn中,交由fn决定何时发布信息
  fn(resolve);
}

Promise.prototype.then = function(successCallback) {
  //新建一个高阶函数处理return出去promise的resolve
  const _handleSuccessCallback = resolve => val => {
    //successCallback是Promise实例调用then调用时传入的successCallback 不是return出去的Promise实例再调用then时候传入的successCallback哦
    const result = successCallback(val);
    //如果result的值是promise 那么就在加一个then进去 执行return出去promise的resolve
    if (result && result instanceof Promise) {
      result.then(_val => {
        resolve(_val);
      })
    } else {
      resolve(result);
    }
  };
  //为容易理解起见 缓存this
  const that = this;
  return new Promise(function(resolve) {
    //如果promise已经决议 那么久立即执行回调 不然就先推入回调数组中
    if (that.status === 'fulfill') {
      _handleSuccessCallback(resolve)(that._val);
    } else {
      that.callBacks.push(_handleSuccessCallback(resolve));
    }
  })
};

虽然我注释写了不少,但估计有的同学会觉得比较绕,这版和之前唯一不同是在于 then 返回一个新的 Promise,这个之前解释过,问题还是不大,难点是在于里面的 _handleSuccessCallback 方法。

先明确问题, Promise.prototype.then 接受一个函数,它既可以是同步的,处理完之后返回一个值(甚至不返还),也可以是异步的,返回一个 Promise 实例,在合适的时机调用 resolve,异步传递值供后面的 then 使用。同步还好说,关键是如何如何解决异步问题呢?要知道返回的 Promise 实例是可以带很多 then 的哦!

先解决后一个问题,其实无论带有多少个 then,根据我们的实现,都是返回最后一个 then 调用后返回的 Promise 实例。至于其中如何调用,其实我们已经实现好了,这有点像递归,处理好逻辑后,让函数不断自己调用自己就好了。

再来解决第一个问题,其实是通过 _handleSuccessCallback 这个函数实现。_handleSuccessCallback 接受一个参数,就是 Promise 构造函数内的 resolve,但请记住,这个 resolvethen 调用后返回的 Promise 实例的 resolve,后称为返回实例。

传参调用后返回一个新的函数,这个新函数也接受一个参数 val,也就是 Promise 实例中决议了的值,这个实例是调用 then 方法的实例,后成为调用 then 实例。新函数在两种情况下会被调用,调用 then 实例状态是 fulfill 时,或是调用 then 实例从 pending 转为 fulfill 时调用构造函数内的 resolve 方法。

当新函数调用时,先将调用 then 实例决议后的值作为参数传给 successCallback 执行,记录结果为 result。如果 result 是一个 Promise 实例,那就添加一个 then ,将 result 决议后的值传给返回实例的 resolve 执行即可。如若不是 Promise 实例,那就更好办了,直接将 result 作为实参传给返回实例的 resolve 调用即可。

至此,两个问题都解决了,无论传入 then 中的函数是异步还是同步的,我们都可以将它调用返回的值或决议后的值传给后面的 then 执行。

写个小例子测试一下:

const p = new Promise(function(res) {
  setTimeout(() => {
    res(123);
  }, 1000);
});

p.then(function(val) {
  console.log(val);
  return new Promise(function(res) {
    setTimeout(() => {
      res(456);
    }, 1000);
  }).then(val => {
    console.log(val);
    return 789;
  })
}).then(val => console.log(val));

浏览器跑一下这个例子,会在1秒后打印123,再1秒后打印出456,然后是789。逻辑整体是没有问题的。

小结

至此,自己实现的 Promise 算是完成啦,当然这是最粗糙的实现,还缺少很多功能,如 rejectPromise.resolvePromise.reject 等。然而通过参考上述的例子,相信看官大人你肯定很容易就能实现其他功能。由于篇幅关系,就不再实现近似的功能了。

其实了解实现还是其次,主要是实现过程中用到的技巧是十分值得学习的。能熟练地异步编程是每一个前端都必须掌握的。感谢各位看官大人看到这里~希望本文对你有所帮助。谢谢!

参考资料

Node.js 实践教程 - Promise 实现

30分钟,让你彻底明白Promise原理