如果你想要学习一门新技术,最好的方式是先了解这门技术是如何诞生的,以及它所解决的问题是什么。了解了这些后,你才能抓住这门技术的本质

所以本文我们就来重点聊聊 JavaScript 引入 Promise 的动机,以及解决问题的几个核心关键点

要谈动机,我们一般都是先从问题切入,那么 Promise 到底解决了什么问题呢?
在正式开始介绍之前,我想有必要明确下,Promise 解决的是异步编码风格的问题,而不是一些其他的问题,所以接下来我们聊的话题都是围绕编码风格展开的。

这段代码之所以看上去很乱,归结其原因有两点:
第一是嵌套调用,下面的任务依赖上个任务的请求结果,并在上个任务的回调函数内部执行新的业务逻辑,这样当嵌套层次多了之后,代码的可读性就变得非常差了。

第二是任务的不确定性,执行每个任务都有两种可能的结果(成功或者失败),所以体现在代码中就需要对每个任务的执行结果做两次判断,这种对每个任务都要进行一次额外的错误处理的方式,明显增加了代码的混乱程度。

原因分析出来后,那么问题的解决思路就很清晰了:

  • 第一是消灭嵌套调用;
  • 第二是合并多个任务的错误处理。


这么讲可能有点抽象,不过 Promise 已经帮助我们解决了这两个问题。
那么接下来我们就来看看 Promise 是怎么消灭嵌套调用和合并多个任务的错误处理的。

Promise 主要通过下面两步解决嵌套回调问题的。
首先,Promise 实现了**回调函数的延时绑定
回调函数的延时绑定在代码上体现就
是先创建 Promise 对象 x1,通过 Promise 的构造函数 executor 来执行业务逻辑;创建好 Promise 对象 x1 之后,再使用 x1.then 来设置回调函数**。示范代码如下:

  1. //创建Promise对象x1,并在executor函数中执行业务逻辑
  2. function executor(resolve, reject){
  3. resolve(100)
  4. }
  5. let x1 = new Promise(executor)
  6. //x1延迟绑定回调函数onResolve
  7. function onResolve(value){
  8. console.log(value)
  9. }
  10. x1.then(onResolve)

其次,需要将回调函数 onResolve 的返回值穿透到最外层。
因为我们会根据 onResolve 函数的传入值来决定创建什么类型的 Promise 任务,创建好的 Promise 对象需要返回到最外层,这样就可以摆脱嵌套循环了。你可以先看下面的代码:
Promise和微任务 - 图1

现在我们知道了 Promise 通过回调函数延迟绑定和回调函数返回值穿透的技术,解决了循环嵌套。

那接下来我们再来看看 Promise 是怎么处理异常的,你可以回顾上篇文章思考题留的那段代码,我把这段代码也贴在文中了,如下所示:

Promise 与微任务

讲了这么多,我们似乎还没有将微任务和 Promise 关联起来,那么 Promise 和微任务的关系到底体现哪里呢?
我们可以结合下面这个简单的 Promise 代码来回答这个问题:

  1. function executor(resolve, reject) {
  2. resolve(100)
  3. }
  4. let demo = new Promise(executor)
  5. function onResolve(value){
  6. console.log(value)
  7. }
  8. demo.then(onResolve)

对于上面这段代码,我们需要重点关注下它的执行顺序。
首先执行 new Promise 时,Promise 的构造函数会被执行,不过由于 Promise 是 V8 引擎提供的,所以暂时看不到 Promise 构造函数的细节。

接下来,Promise 的构造函数会调用 Promise 的参数 executor 函数。
然后在 executor 中执行了 resolve,resolve 函数也是在 V8 内部实现的,那么 resolve 函数到底做了什么呢?
我们知道,执行 resolve 函数,会触发 demo.then 设置的回调函数 onResolve,所以可以推测,resolve 函数内部调用了通过 demo.then 设置的 onResolve 函数

不过这里需要注意一下,由于 Promise 采用了回调函数延迟绑定技术,所以在执行 resolve 函数的时候,回调函数还没有绑定,那么只能推迟回调函数的执行。

这样按顺序陈述可能把你绕晕了,下面来模拟实现一个 Promise,我们会实现它的构造函数、resolve 方法以及 then 方法,以方便你能看清楚 Promise 的背后都发生了什么

这里我们就把这个对象称为 Bromise,下面就是 Bromise 的实现代码:

  1. function Bromise(executor) {
  2. var onResolve_ = null
  3. var onReject_ = null
  4. //模拟实现resolve和then,暂不支持rejcet
  5. this.then = function (onResolve, onReject) {
  6. onResolve_ = onResolve
  7. };
  8. function resolve(value) {
  9. //setTimeout(()=>{
  10. onResolve_(value)
  11. // },0)
  12. }
  13. executor(resolve, null);
  14. }

也正是因为此,我们要改造 Bromise 中的 resolve 方法,让 resolve 延迟调用 onResolve_
要让 resolve 中的 onResolve 函数延后执行,可以在 resolve 函数里面加上一个定时器,让其延时执行 onResolve 函数,你可以参考下面改造后的代码:

function resolve(value) {
setTimeout(()=>{
onResolve_(value)
},0)
}

上面采用了定时器来推迟 onResolve 的执行,不过使用定时器的效率并不是太高,好在我们有微任务,所以 Promise 又把这个定时器改造成了微任务了,这样既可以让 onResolve_ 延时被调用,又提升了代码的执行效率。
这就是 Promise 中使用微任务的原由了。

Promise 中的 resolve 函数其实就是执行了 then() 方法中传进来的 onResolve() 函数,但是用户调用 resolve() 往往是在构造函数的参数 execute() 函数中,而构造函数执行时,onResolve 函数还没被赋值。 因此在 resolve() 中执行 onResolve() 函数的代码需要用一个微任务来执行。 所以执行resolve方法,会创建一个微任务。

总结

首先,我们回顾了 Web 页面是单线程架构模型,这种模型决定了我们编写代码的形式——异步编程。
基于异步编程模型写出来的代码会把一些关键的逻辑点打乱,所以这种风格的代码不符合人的线性思维方式。接下来我们试着把一些不必要的回调接口封装起来,简单封装取得了一定的效果,不过,在稍微复制点的场景下依然存在着回调地狱的问题。

然后我们分析了产生回调地狱的原因:
多层嵌套的问题;
每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。

Promise 通过回调函数延迟绑定回调函数返回值穿透错误“冒泡”技术解决了上面的两个问题。

最后,我们还分析了 Promise 之所以要使用微任务是由 Promise 回调函数延迟绑定技术导致的。
(这么看来,是因为Promise要用到微任务,所以Promise才和微任务产生了关系。)

思考时间

终于把 Promise 讲完了,这一篇文章非常有难度,所以需要你课后慢慢消消化,再次提醒,Promise 非常重要。
那么今天我给你留三个思考题:
Promise 中为什么要引入微任务?
Promise 中是如何实现回调函数返回值穿透的?
Promise 出错后,是怎么通过“冒泡”传递给最后那个捕获异常的函数?

这三个问题你不用急着完成,可以先花一段时间查阅材料,然后再来一道一道解释。

搞清楚了这三道题目,你也就搞清楚了 Promise。