深入理解 JavaScript Promises:从基础到并行处理

作者:拾光

时间:2026-10-29

浏览:578

本文深入解析 JavaScript Promises 的核心机制,涵盖其三种状态、then 方法的链式调用原理及错误处理。文章对比了传统回调函数与 Promises 在解决“厄运金字塔”问题上的差异,并探讨了通过命名回调与链式调用提升代码复用性的技巧。最后,重点介绍了利用 jQuery.when 等方法实现多个异步操作的并行执行与结果聚合。

文章目录

Promises 的核心概念与状态机制

如今,几乎每一位 JavaScript 开发者乃至其周围的人都已熟悉 Promises。若你尚不知情,不久后也将了解。Promises 的概念最初由 CommonJS 小组在 Promises/A 规范中提出。尽管 Promises 常被用作管理异步操作回调的手段,但其设计初衷远不止于此。事实上,由于 Promises 的多功能性,许多人在阅读我关于 Promises 的文章后指出,他们觉得我“未触及 Promises 的核心”。那么,Promises 的真正核心究竟是什么?

关于 Promises 的基础知识

在探讨 Promises 的核心之前,先简要介绍其工作原理。根据 Promise/A 规范,Promise 是一个对象,且必须包含一个 then 方法。then 方法接收三个参数:成功回调、失败回调以及进度回调(规范未强制要求实现进度回调,但许多库已支持)。每次调用 then 都会返回一个新的 Promise 对象。 Promise 处于三种状态之一:未完成(pending)、已完成(fulfilled)或失败(rejected)。Promise 初始状态为未完成。若操作成功,状态转为已完成;若失败,则转为失败态。当 Promise 转为已完成时,所有注册的成功回调将被调用,并传入成功结果值。此外,若 Promise 已完成,后续注册的成功回调将立即被调用。

当 Promise 转为失败态时,行为类似,但调用的是失败回调而非成功回调。对于支持进度特性的实现,Promise 在离开未完成状态前的任何时刻均可更新进度。当进度更新时,所有进度回调将接收进度值并被立即调用。进度回调的处理方式不同于成功和失败回调:若在进度更新后注册新的进度回调,该新回调仅会在注册后接收已更新的进度值。 我们不再深入探讨 Promise 状态管理的具体实现,因为这不在规范范围内且各实现存在差异。在后续示例中,你将看到其具体实现方式,目前只需了解上述内容即可。

从回调到 Promises:解决厄运金字塔

处理回调

如前所述,处理异步操作的回调是 Promises 最基本且常见的用途。让我们将标准回调与采用 Promises 的回调进行对比。


// Normal callback usage
asyncOperation(function() {
  // Here's your callback
});
// Now `asyncOperation` returns a promise
asyncOperation().then(function(){
  // Here's your callback
});

仅看此例,可能无人会认为使用 Promises 有显著优势。其好处似乎仅在于“then”使异步操作完成后的回调调用更加直观。然而,这反而增加了代码量(抽象应使代码更简洁,不是吗?),且 Promises 的性能略低于标准回调。

但这不应成为阻碍。若 Promises 仅能做到此点,本文便无存在必要。

厄运的金字塔


// Normal callback usage => PYRAMID OF DOOOOOOOOM
asyncOperation(function(data){
  // Do some processing with `data`
  anotherAsync(function(data2){
    // Some more processing with `data2`
    yetAnotherAsync(function(){
      // Yay we're finished!
    });
  });
});
// Let's look at using promises
asyncOperation()
.then(function(data){
  // Do some processing with `data`
  return anotherAsync();
})
.then(function(data2){
  // Some more processing with `data2`
  return yetAnotherAsync();
})
.then(function(){
  // Yay we're finished!
});

如前所见,使用 Promises 使代码结构扁平化且更具可读性。这之所以可行,是因为如前所述,then 返回一个 Promise,因此可以不断链式调用 then。由 then 返回的 Promise 装载了回调返回的值。若回调返回一个 Promise(如本例所示),则 then 返回的 Promise 将装载与回调返回的 Promise 相同的值。这听起来复杂,我将逐步解释。

异步操作返回一个 Promise 对象。因此,我们在该 Promise 上调用 then,并传入回调函数;then 也返回一个 Promise。当异步操作结束时,它将数据装载到该 Promise。随后(第一次)回调被调用,数据作为参数传入。若回调无返回值,则 then 返回的 Promise 将立即以无值状态装载。若回调返回非 Promise 值,则 then 返回的 Promise 将立即装载该数值。若回调返回一个 Promise(如本例),则 then 返回的 Promise 将等待直到回调返回的 Promise 被完全装载。一旦回调的 Promise 被装载,其值(本例中为 data2)将被提交给 then 的 Promise。然后 then 中的 Promise 装载了 data2。依此类推。这听起来复杂,但实则简单。若你仍不理解,我深表歉意,或许我不是最佳讲解者。

命名回调与链式调用的复用性优化

使用命名回调替代

但显然,Promises 并非使结构扁平化的唯一方法。在我发表一篇关于 Promises 解决厄运金字塔问题的文章后,有人评论道……

我认为 Promises 有时是有用的,但“嵌套”回调的问题(圣诞树综合症)仅通过用命名函数替代匿名函数作为参数即可常规处理:


asyncCall( param1, param2, HandlerCallback );
function HandlerCallback(err, res){
// do stuff
}

其示例仅展示了一层深度,但原理正确。让我们扩展我之前的示例,使其更清晰。

命名回调


// Normal callback usage => PYRAMID OF DOOOOOOOOM
asyncOperation(handler1);
function handler1(data) {
  // Do some processing with `data`
  anotherAsync(handler2);
}
function handler2(data2) {
  // Some more processing with `data2`
  yetAnotherAsync(handler3);
}
function handler3() {
  // Yay we're finished!
}

看看上面的代码!他们完全正确!这是一个扁平的结构,但这里存在一个我之前从未注意到的老式回调示例中的问题:依赖性与复用性。依赖性和复用性是相互关联的可逆类型。依赖越少,复用性越大。在以上示例中,handler1 依赖 handler2,handler2 依赖 handler3。这意味着除非 handler2 也存在,否则 handler1 无法用于任何目的。如果你不打算重用它们,给函数命名又有何意义?

更糟糕的是,handler1 根本不关心 handler2 内部发生了什么。除了与它异步工作外,它根本不需要 handler2。因此,让我们消除这些依赖性,并通过使用 Promises 使函数更具复用性。

链式回调


asyncOperation().then(handler1).then(handler2).then(handler3);
function handler1(data) {
  // Do some processing with `data`
  return anotherAsync();
}
function handler2(data2) {
  // Some more processing with `data2`
  return yetAnotherAsync();
}
function handler3() {
  // Yay we're finished!
}

这样看起来好多了?如果存在其他函数,现在 handler1 和 handler2 互不相关。想看看它们是否真的出色吗?现在 handler1 可在不需要 handler2 的情况下使用。相反,在 handler1 操作后,我们可以使用另一个 handler。

复用函数


asyncOperation().then(handler1).then(anotherHandler);
function handler1(data) {
  // Do some processing with `data`
  return anotherAsync();
}
function anotherHandler(data2) {
  // Do some really awesome stuff that you've never seen before. It'll impress you
}

现在 handler1 已从 handler2 脱离,可用于更多场景,特别是那些由 handler2 提供但我们不想要的功能。这就是复用性!评论家解决代码易读性的唯一方法是通过消除缩进。我们不想仅为了缩进而消除缩进。多层缩进仅是某些事情错误的标志,问题不一定在于其本身。它就像由脱水引起的头痛。真正的问题是脱水,而非头痛。解决方法是补充水分,而非服用止痛药。

并行异步操作与 jQuery.when 的应用

并行异步操作

在我之前提到的文章中,我将 Promises 与事件在处理异步操作方面进行了比较。遗憾的是,按照那些曾提到过的人在评论里的说法,我的比较并不成功。我描述了 Promises 的力量,接着转到事件来描述它们的力量,就像在我的特别项目中用到的那样。没有比较和对比。一位评论者写道(修改了一点语法错误):

我想用帖子中的例子是一个坏的对照。有篇论文证明了 Promises 的价值,如果按下虚构的“启动服务器按钮”,将不仅仅是启动一个 Web 服务器,还有一个数据库服务器,当它们都在运行时只是更新了 UI。

他完全正确。事实上我没有比较那两种情况。那篇文章的要点实际在于说明 Promises 不是异步操作的唯一机制,而且在一些情况下,它们也不一定是最好的。在这个评论者指出的情况下,Promises 当然是最佳的解决办法。我们来看看他说的是什么

jQuery 具有一个名为 when 的方法,可以带上任意数量的 Promise 参数,并返回一个单一的 Promise。如果任何一个 Promise 传入失败,when 返回的 Promise 也会失败。如果所有的 Promises 被装载,那么每个值都将会按照 Promises 被定义的顺序传递给附加的回调。

以并行的方式执行无数的异步操作非常有用,然后只要在它们之中的每一个结束之后继续执行回调。我们看一个简单的例子。

jQuery.when


// Each of these async functions return a promise
var promise1 = asyncOperation1();
var promise2 = asyncOperation2();
var promise3 = asyncOperation3();
// The $ refers to jQuery
$.when(promise1, promise2, promise3).then(
  function(value1, value2, value3){
    // Do something with each of the returned values
  }
);

人们经常说这是 Promises 带来的最好的东西之一,也是 Promises 的一部分重要的意义所在。我也认为这是个简化了大量操作的好特性,但是这种 when 方法的机制 根本就没有在任何 Promises 规范中提到,所以我不认为它是 Promises意义所在。有一个规范提到了 when 方法,但是和上面的完全不同。就我所知,jQuery 是唯一的实现了这种 when 方法的库。其他的 Promises 库,例如 Q, Dojo, 和 when 依照 Promises/B spec 实现了 when 方法, 但是并没有实现注释者提及的 when 方法。但是,Q 库有一个 all方法,when.js 也有一个 parallel方法,与上面的 jQuery.when 方法作用一样,只是它们接受一个数组类型的参数,而不是任意数量的参数。

Promises 的本质:值的异步表示

值的表示

Promises 是处理以下场景的更好的方法:

"我想在这个数据库中找一个用户,但find方法是异步的。"

因此,这里我们有了一个不能立刻返回值的find方法。但最终它确实"返回"了一个数值(通过一个回调的方式),而你希望以某种方式处理那个数值。现在,通过使用一个回调,你能定义一个继续部分,或者说“一些将在以后时间里处理那个数值的代码”

Promise改变了那种“嘿,这里是一些你会发现你用来处理返回数值的代码”。它们是一些允许"find"方法说“嘿,我将忙着找你要找的信息,但与此同时你能继续等着返回结果,而且你能同时以任何你希望的方式处理它,就像实际的东西!”

Promise代表了真实的数值。那就是陷阱。它们工作在你像处理实际东西一样处理Promise的时候。Promise的JavaScript实现期待你给它传递一个回调函数,这只是一个“巧合”,它不是重要的事情。

我相信这真的就是promise的重点。为什么?读一读 Promise/A规范 的第一句“一个promise代表了一个操作的一次完成最终返回的数值。“使它有点明显了,是不是?好吧,即使那就是重点,那也不能阻止我在后面本文中呈现其他人的见解。不管怎么说,我们再多谈论这个思想一点。

结论

promise的重点是它代表一个操作返回的最终结果值,但使用它们的原因是使同步操作更好的并行。自从异步编程进入此场景,到处都是弹出的回调,以奇怪的方式遮住我们的代码。Promise是一种改变其的方法。Promise允许我们以同步的方式写代码,同时给予我们代码的异步执行。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章