深入解析JavaScript Promise:从回调地狱到链式调用

作者:拾光

时间:2026-10-25

浏览:99

本文深入解析JavaScript Promise机制,探讨其如何解决单线程异步编程中的回调地狱问题。详细阐述Promise的三种状态(pending、fulfilled、rejected)转换规则,剖析then、catch、finally等原型方法的行为,并介绍all、race、any等常用API,帮助开发者掌握现代异步代码的最佳实践。

文章目录

为什么要使用Promise?

JavaScript采用单线程模型,同一时间仅能处理一个任务,这会导致其他任务被阻塞。因此,网络请求和浏览器事件等通常以异步方式执行,开发者可以通过异步回调函数来处理这些操作。

设想这样一个场景:通过六层人际关系网络可以认识任何人,但我们无法预知每次联系下一人的耗时,这构成了一个典型的异步操作场景。

该场景的代码实现如下:

  function ConnectPeople(i) {
    console.log(`我联系到了第${i}个人`);
    return i + 1;
  }
  let i = 1;
  setTimeout(() => {
    const result1 = ConnectPeople(i);
    setTimeout(() => {
      const result2 = ConnectPeople(result1);
      setTimeout(() => {
        const result3 = ConnectPeople(result2);
        setTimeout(() => {
          const result4 = ConnectPeople(result3);
          setTimeout(() => {
            const result5 = ConnectPeople(result4);
            setTimeout(() => {
              const result6 = ConnectPeople(result5);
              setTimeout(() => {
                const result7 = ConnectPeople(result6);
                setTimeout(() => {
                  const result8 = ConnectPeople(result7);
                  setTimeout(() => {
                    const result9 = ConnectPeople(result8);
                    setTimeout(() => {
                      const result10 = ConnectPeople(result9);
                    }, 10000);
                  }, 5000);
                }, 3000);
              }, 2000);
            }, 3000);
          }, 2000);
        }, 1000);
      }, 500);
    }, 2000);
  }, 1000);

为什么要使用Promise?

如上所示,当我们联系到了第一个人后,再去联系第二个人,然后再去联系第三个人...直到我联系到了10个人。乍一看,代码好像还挺规整,但是如果100个人,1000个人呢?由于回调很多,函数作为参数层层嵌套,就陷入了回调地狱。这种情况下,就像是金字塔一样的代码非常不利于阅读。

幸运的是,我们有相应的解决方案。

利用Promise解决异步控制问题

什么是Promise?

Promise对象的主要用途是通过链式调⽤的结构,它将原本嵌套的异步处理流程转化为“对象.then().then()...”的链式结构。尽管仍依赖回调函数,但这种结构将嵌套转化为连续调用,使异步执行流程在视觉上呈现为上下顺序,提升了代码的可读性。

因此,Promise的核心作用是解决“回调地狱”,其方式是将回调嵌套拆分为链式调用,从而实现按顺序执行的异步流程控制。如下代码所示,使用Promise后,代码从金字塔形式转变为自上而下的执行流。

  function ConnectPeople(i) {
    console.log(`我联系到了第${i}个人`);
    return i + 1;
  }
  const p = new Promise((resolve) => {
    setTimeout(() => {
      resolve(ConnectPeople(1));
    }, 1000);
  });
  p.then((v1) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(ConnectPeople(v1));
      }, 1000);
    });
  }).then((v2) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(ConnectPeople(v2));
      }, 1000);
    });
  });

为什么要使用Promise?

Promise的结构

基于上述代码案例,我们发现Promise需要通过new关键字并传入参数来创建。尝试打印window对象console.log(window)(window对象在浏览器中具有双重身份:既是ECMAScript中的Global对象,也是浏览器窗口的JavaScript接口),可以发现其中存在Promise构造函数。

为什么要使用Promise?

初始化Promise时需要传入一个函数,示例如下:

const p = new Promise(fn) // fn是初始化的时候调用的函数,它是同步的回调函数

回调函数

什么是回调函数?

JavaScript中的回调函数结构默认是同步的。由于JavaScript单线程异步模型的特性,若要编写异步代码,必须使用回调嵌套的形式。因此,回调函数不一定是异步代码,但异步代码必然采用回调函数结构。

为什么异步代码一定是回调函数结构?

JavaScript采用单线程异步模型,严格遵循同步代码在前、异步代码在后的执行顺序。如果使用默认的上下结构,将无法直接获取异步回调中的结果。

如下代码执行时,会先执行同步代码,异步代码会被挂起并继续执行后续同步代码。1秒时挂起的任务进入队列,2秒时继续执行同步代码打印1,随后从任务队列中取出任务将num变为100并打印num。因此,实际效果是2秒后先打印1再打印100。

var num = 1;
setTimeout(()=>{
    num = 100
    console.log(num)
},0)
var d = new Date().getTime()
var d1 = new Date().getTime()
while ( d1 - d < 1000 ) {
    d1 = new Date().getTime()
}
console.log(num) // 1

剖析Promise

翻译promise,结果是承诺,保证,红宝书中的解释是期约。

Promise包含三种状态:

  • pending(待定):初始状态。
  • fulfilled(兑现):已完成,通常代表任务成功执行。当初始化函数中的resolve()执行时,状态变为fulfilled,且.then注册的回调开始执行,resolve传递的参数成为回调函数的形参。
  • rejected(拒绝):通常代表任务执行失败。调用reject()时,catch注册的函数触发,reject传递的内容成为回调函数的形参。

三种状态之间的关系:

对象创建后,同一个Promise对象只能从pending状态变更为fulfilled或rejected中的一种,且状态一旦变更便不可逆。此时Promise对象的流程执行完成,并执行finally函数。

为什么要使用Promise?

打印Promise对象发现,其构造函数中定义了all、allSettled、any、race、reject、resolve方法(实例方法),原型上存在catch、finally、then方法(原型方法)。

原型方法——catch、finally、then

首先看以下代码:

  new Promise(function (resolve, reject) {
    resolve();
    reject();
  })
    .then(function () {
      console.log("then执⾏");
    })
    .catch(function () {
      console.log("catch执⾏");
    })
    .finally(function () {
      console.log("finally执⾏");
    });

执行后依次打印then执行->finally执行,发现.catch的回调并未执行。

再看如下代码:

  new Promise(function (resolve, reject) {
    reject();
    resolve();
  })
    .then(function () {
      console.log("then执⾏");
    })
    .catch(function () {
      console.log("catch执⾏");
    })
    .finally(function () {
      console.log("finally执⾏");
    });

这段代码与之前的唯一区别在于Promise中的回调先执行resolve()还是reject(),打印结果是catch执行->finally执行,发现.then的回调未执行。

如果Promise的回调既不执行reject()也不执行resolve()呢?

会发现没有任何输出。

注意:Promise.prototype.catch()实际上是语法糖,相当于调用Promise.prototype.then(null, onRejected)。.then可以传入两个回调函数,第一个在resolve()后执行,第二个在reject()后执行,二者互斥。

这是因为Promise的异步回调部分如何执行,取决于初始化函数中的操作。一旦调用resolve,后续再执行reject不会影响then的执行,catch也不会执行,反之亦然。

而在初始化回调函数中,如果不执行任何操作,Promise的状态将保持pending,所有注册的回调函数都不会执行。

由此可见,执行完resolve()之后才能执行.then的回调;执行reject()之后才能执行.catch的回调;finally()的回调会在执行完.then或.catch之后执行。

此时我们会想,是否可以将resolve或reject的调用设定在异步函数内,从而解决回调地狱问题?

因此我们尝试如下代码:

  new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log(111);
      resolve();
    }, 2000);
  })
    .then(function () {
      return new Promise(function (resolve, reject) {
        setTimeout(() => {
          console.log(222);
          resolve();
        }, 2000);
      });
    })
    .then(function () {
      return new Promise(function (resolve, reject) {
        setTimeout(() => {
          console.log(333);
          resolve();
        }, 2000);
      });
    })
    .catch(function () {
      console.log("catch执⾏");
    })
    .finally(function () {
      console.log("finally执⾏");
    });

上述代码每隔2秒依次打印111->222->333 finally执行。333执行后立刻执行finally。

为什么要在.then的回调函数中return一个Promise?

因为下一个异步执行需要等待前一个异步执行完毕后才调用,我们需要利用resolve来控制.then执行的时机。

如果不指明return返回值,它会返回什么?又是如何实现链式调用的?

看以下代码:

  const p2 = new Promise((resolve, reject) => {
    resolve();
  });
  const p3 = p2.then(() => {
    console.log("resolved");
  });
  console.log(p3, 111);

p2.then的回调函数中没有return,但一般函数返回值默认返回undefined,而undefined中不存在.then方法。

因此我们查看p3里面到底是什么。有人可能会想,.then是异步调用,属于微任务,访问p3是否正确?

我们打印p3,会看到如下信息:

为什么要使用Promise?

第一行p3的状态还是pending,当我们点开,发现已经变成了fulfilled了,因为引用类型是按地址访问的,当我们点开的时候会发现指向这个地址里最后的数据是什么。普通对象同理。

如下所示,我们console的时候a对象的name还是a,但是我们点开后发现程序执行完后a对象的实际name变成了b。

  const a = { name: "a" };
  console.log(a);
  a.name = "b";

为什么要使用Promise?

回归正传,我们发现p3里面有3个字段,[[Prototype]]我们很熟悉,这个是一个指向当前对象原型的指针。在大多数游览器中是可以通过__proto__访问到的。

我们尝试着去访问:

  console.log(p3, 111);
  console.log(p3.__proto__);
  console.log(p3.__proto__ === Promise.prototype); // true

为什么要使用Promise?

我们可以看到.then默认返回的有3个字段,然后通过原型链来实现链式调用:

  • [[Prototype]]代表Promise的原型对象
  • [[PromiseState]]代表Promise对象当前的状态
  • [[PromiseResult]]代表Promise对象的值,分别对应resolve或reject传入的结果

本质就是在我们调用这些支持链式调用的函数的结尾时,它又返回了一个包含它自己的对象或者是一个新的自己,这些方式都可以实现链式调用。

中断链式调用的方式:

中断的方式可以使用抛出一个异常或返回一个rejected状态的Promise对象

链式调用的基本形式:

  • 只要有then()并且触发了resolve,整个链条就会执行到结尾,这个过程中的第一个回调函数的参数是resolve传入的值
  • 后续每个函数都可以使用return返回一个结果,如果没有返回结果的话下一个then中回调函数的参数就是undefined
  • 返回结果如果是普通变量,那么这个值就是下一个then中回调函数的参数
  • 如果返回的是一个Promise对象,那么这个Promise对象resolve的结果会变成下一次then中回调的函数的参数
  • 如果then中传入的不是函数或者未传值,Promise链条并不会中断then的链式调用,并且在这之前最后一次的返回结果,会直接进入离它最近的正确的then中的回调函数作为参数

前面几条我们都能懂,第5条什么意思的?看以下代码:

  const p2 = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
  });
  p2.then(() => {
    console.log(2);
    return 123;
  })
    .then()
    .then("456")
    .then((res) => {
      console.log(res);
    });

为什么要使用Promise?

发现只打印了1 2 和 123,return的123进入了最后一个.then的回调函数中作为参数。

resolve和reject

通过上文我们已经知道resolve和reject能够更改Promise的状态,而Promise的状态是不可逆且私有的。因此必须在Promise内部调用resolve或reject。

当然,resolve和reject也可以传入参数,传入的参数会变为.then或.catch的回调函数中的参数。

如果传入一个Promise作为参数呢???

resolve()

实际上,如果在resolve中传入一个promise,其行为相当于是一个空包装。Promise.resolve()可以说相当于是一个幂等方法,会保留传入期约的状态。

let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p)); // true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p))); // true

reject()

会实例化一个拒绝的期约并抛出一个异步错误,不能通过try...catch捕获,只能通过拒绝处理程序捕获。

如果给reject传入一个promise,则这个promise会成为返回的拒绝promise的理由。

  const p1 = new Promise(() => {});
  const p2 = Promise.resolve(111);
  const r3 = Promise.reject(p1);
  const r4 = Promise.reject(p2);
  console.log(r3);
  console.log(r4);

为什么要使用Promise?

Promise常用API——all()、allSettled()、any()、race()

all()

假设有一个需求:一个页面需要请求3个接口才能渲染,且要求3个接口必须全部返回。如果通过链式调用方式,接口1请求完再请求接口2,最后请求接口3,全部成功后再渲染页面,这种方式非常耗时。因此出现了all方法来解决这一问题。

Promise.all([promise对象,promise对象,...]).then(回调函数)

Promise.all()的参数是一个Promise数组,只有数组中所有的Promise的状态变成了fulfilled之后才会执行.then回调的第一个回调函数,并将每个Promise结果的数组变为回调函数的参数。如果Promise中有一个rejected,那么就会触发.catch()的回调。

  let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("第⼀个promise执⾏完毕");
    }, 1000);
  });
  let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("第⼆个promise执⾏完毕");
    }, 2000);
  });
  let p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("第三个promise执⾏完毕");
    }, 3000);
  });
  Promise.all([p1, p3, p2])
    .then((res) => {
      console.log(res);
    })
    .catch(function (err) {
      console.log(err);
    });
    // 3s后打印 ['第⼀个promise执⾏完毕', '第三个promise执⾏完毕', '第⼆个promise执⾏完毕']

race()

race()方法与all()方法的使用格式相同,不同的是,回调函数的参数是promise数组中执行最快的promise的返回值,它的状态可能是fulfilled也有可能是rejected,但返回的是最快的结果。

根据race这个单词就能理解,相当于一群promise进行比赛,谁先到终点第一就是谁,不管是男是女。

  //promise.race()相当于将传⼊的所有任务进行一个竞争
  let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("第⼀个promise执⾏完毕");
    }, 5000);
  });

  let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("第⼆个promise执⾏完毕");
    }, 2000);
  });
  let p3 = new Promise((resolve) => {
    setTimeout(() => {
      resolve("第三个promise执⾏完毕");
    }, 3000);
  });
  Promise.race([p1, p3, p2])
    .then((res) => {
      console.log(res);
    })
    .catch(function (err) {
      console.error(err);
    });
    // 2秒后打印第二个promise执行完毕

allSettled()

该方法需要传入所有不在pendding状态的promise数组,然后通过该方法可以知道数组中的promise的当前状态。

当有多个彼此不依赖的异步任务成功完成时,或者总是想知道每个promise的结果时,通常使用它。

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];

Promise.allSettled(promises).
  then((results) => results.forEach((result) => console.log(result.status)));

// "fulfilled"
// "rejected"

any()

这个方法目前还是实验性的,不是所有的游览器都能够支持。

接受一个promise数组,只要有一个promise的状态变成了fulfilled,那么这个方法就会返回这个promise;

如果所有的promise的状态都是rejected,那么就返回失败的promise,并且把单一的错误集合在一起。

const pErr = new Promise((resolve, reject) => {
  reject("总是失败");
});
const pSlow = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, "最终完成");
});
const pFast = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "很快完成");
});
Promise.any([pErr, pSlow, pFast]).then((value) => {
  console.log(value);
})
// 很快完成

到此这篇关于深入学习JavaScript中的promise的文章就介绍到这了,更多相关JavaScript promise内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章