JavaScript Promise 详解:从回调地狱到链式调用与并行处理

作者:拾光

时间:2026-10-29

浏览:211

本文详解 JavaScript Promise 的核心机制,包括待定、拒绝和完成三种状态。通过对比传统回调函数导致的“回调地狱”,展示如何利用 Promise 链实现线性、易读的异步代码。文章进一步探讨了 Promise 在并行任务处理中的应用,以及不同 Promise 库(如 Q、kew)之间的互操作策略,帮助开发者掌握现代异步编程的最佳实践。

文章目录

Promise 概述

许多编程语言为了将异步处理模拟为常规的顺序执行,都提供了被称为 promises、deferreds 或 futures 的工具库。在 JavaScript 中,Promises 有助于实现关注点分离,从而替代紧密耦合的接口。本文主要介绍基于 Promises/A 标准的 JavaScript Promises 实现。[http://wiki.commonjs.org/wiki/Promises/A]

Promise 的典型应用场景包括:

  • 执行规则
  • 多重远程验证
  • 超时处理
  • 远程数据请求
  • 动画效果
  • 将事件逻辑与应用逻辑解耦
  • 消除回调函数的“恐怖三角”(回调地狱)
  • 控制并行的异步操作

Promise 状态与行为

JavaScript Promise 是一个承诺在未来返回值的对象,它是一个具有明确定义行为的数据结构。Promise 存在三种可能的状态:

  1. Pending(待定)
  2. Rejected(拒绝)
  3. Resolved(已完成)

一旦 Promise 进入拒绝或完成状态,即被视为已解决(settled)。Promise 只能从待定状态转变为已解决状态,且状态一旦改变便不再变动。即使对应的处理早已完成,Promise 对象仍可能存在,这意味着我们可以多次获取处理结果。通过调用 promise.then() 可以获取结果,该函数仅在对应处理结束时返回。我们可以灵活地串联多个 Promise,这些串联的“then”函数应返回一个新的 Promise 或原始的 Promise。借助这种模式,我们可以像编写同步代码一样编写异步代码,主要通过组合 Promise 来实现:

  • 堆栈式任务:代码中多处散落,但对应同一个 Promise。
  • 并行任务:多个 Promise 返回同一个父 Promise。
  • 串行任务:一个 Promise 执行完毕后,接着执行另一个 Promise。
  • 上述几种模式的组合。

回调函数的问题与 Promise 的优势

为什么要采用这种看似复杂的方式?仅使用基本的回调函数不行吗?

回调函数存在的问题

回调函数适用于简单的重复性事件,例如根据点击使表单生效,或保存 REST 调用的结果。然而,回调函数会使代码形成链条:一个回调函数调用 REST 函数,并为该 REST 函数设置新的回调函数,新回调函数再调用另一个 REST 函数,依此类推。代码的横向增长往往大于纵向增长。虽然回调函数看起来简单,但当我们急需一个结果并立即在下一行计算中使用它时,问题便显现出来。


'use strict';
var i = 0;
function log(data) {console.log('%d %s', ++i, data); };
 
function validate() {
  log("Wait for it ...");
  // Sequence of four Long-running async activities
  setTimeout(function () {
   log('result first');
   setTimeout(function () {
     log('result second');
     setTimeout(function () {
      log('result third');
      setTimeout(function () {
        log('result fourth')
      }, 1000);
     }, 1000);
   }, 1000);
  }, 1000);
 
};
validate();

我使用 timeout 来模拟异步操作。管理异常非常痛苦,很容易遗漏下游行为。当我们编写回调时,代码组织变得混乱。图 2 显示了一个可以在 NodeJS REPL 中运行的模拟验证流程。在下一节,我们将探讨如何从“金字塔式”(pyramid-of-doom)模式迁移到连续的 Promise 链。

Figure


'use strict';
var i = 0;
function log(data) {console.log('%d %s', ++i, data); };
 
// Asynchronous fn executes a callback result fn
function async(arg, callBack) {
  setTimeout(function(){
   log('result ' + arg);
   callBack();
  }, 1000);
};
 
function validate() {
  log("Wait for it ...");
  // Sequence of four Long-running async activities
  async('first', function () {
   async('second',function () {
     async('third', function () {
      async('fourth', function () {});
     });
   });
  });
};
validate();

在 NodeJS REPL 执行的结果


$ node scripts/examp2b.js
1 Wait for it ...
2 result first
3 result second
4 result third
5 result fourth
$

 

我曾遇到过 AngularJS 动态验证的情况,根据对应表的值动态限制表单项的值。限制项的有效值范围定义在 REST 服务上。

我编写了一个调度器,根据请求的值操作函数栈,以避免回调嵌套。调度器从栈中弹出函数并执行。函数的回调在结束时重新调用调度器,直到栈被清空。每次回调都记录所有从远程验证调用返回的验证错误。

使用 Promises 进行链式调用

我认为我编写的这种方案是一种反模式。如果使用 Angular 的 $http 调用提供的 Promise,整个验证过程的思维会更接近线性形式,类似于同步编程。扁平化的 Promise 链更具可读性。继续... 使用 Promises

本文采用了 kew Promise 库。Q 库同样适用。要使用该库,首先使用 npm 将 kew 库导入到 NodeJS,然后加载代码到 NodeJS REPL。

Figure


'use strict';
var Q = require('kew');
var i = 0;
 
function log(data) {console.log('%d %s', ++i, data); };
 
// Asynchronous fn returns a promise
function async(arg) {
  var deferred = Q.defer();
  setTimeout(function () {
    deferred.resolve('result ' + arg);\
  }, 1000);
  return deferred.promise;
};
 
// Flattened promise chain
function validate() {
  log("Wait for it ...");
  async('first').then(function(resp){
    log(resp);
    return async('second');
  })
  .then(function(resp){
    log(resp);
    return async('third')
  })
  .then(function(resp){
    log(resp);
    return async('fourth');
  })
  .then(function(resp){
    log(resp);
  }).fail(log);
};
validate();

输出结果与使用嵌套回调时相同:


$ node scripts/examp2-pflat.js
1 Wait for it ...
2 result first
3 result second
4 result third
5 result fourth
$

该代码稍微“长高”了一些,但我认为它更易于理解和修改。它也更容易添加适当的错误处理。在链的末尾调用 fail 用于捕获链中的错误,但我也可以在任何一个 then 里面提供一个 reject 的处理函数来进行相应处理。

服务器 或 浏览器

Promises 在浏览器中的表现与在 NodeJS 服务器中一样有效。下面的地址,http://jsfiddle.net/mauget/DnQDx/,指向 JSFiddle 的一个展示如何使用 Promise 的 Web 页面。JSFiddle 的所有代码均可修改。我故意进行了随意操作。你可以尝试几次得到相反的结果。它可以扩展到多个 Promise 链,就像前面的 NodeJS 例子一样。

Promise 概述

并行 Promises 处理

并行 Promises

考虑一个异步操作喂养另一个异步操作的情况。让后者包括三个并行异步行为,反过来,再喂最后一个行动。只有当所有平行的子请求通过时,整体流程才能通过。这灵感自偶遇一打 MongoDB 操作。其中有些是合格的并行操作。我实现了 Promises 的流流程图。

Promise 概述

我们如何模拟图中中心行的并行 Promises?关键在于,大多数 Promise 库都提供全功能,它可以产生一个包含一组子 Promises 的父 Promise。当所有子 Promises 通过时,父 Promise 通过。如果有一个子 Promise 拒绝,父 Promise 也拒绝。

让十个并行的 Promises 每个都包含一个文字 Promise。只有当十个子类通过,或者如果任何子类拒绝,最后的 then 方法才能完成。

Figure


var promiseVals = ['To ', 'be, ', 'or ',
  'not ', 'to ', 'be, ', 'that ',
  'is ', 'the ', 'question.'];
 
var startParallelActions = function (){
  var promises = [];
 
  // Make an asynchronous action from each literal
  promiseVals.forEach(function(value){
    promises.push(makeAPromise(value));
  });
 
  // Consolidate all promises into a promise of promises
  return Q.all(promises);
};
 
startParallelActions ().then( . . .

下面的地址,http://jsfiddle.net/mauget/XKCy2/,针对 JSFiddle 在浏览器中运行十个并行 Promises,随机拒绝或通过。这里有完整的代码用于检查和变化 if 条件。重新运行,直到你得到一个相反的完成。Promise 概述

Promise 库互操作与结语

孕育 Promise

许多 API 返回的 Promise 都有一个 then 函数——它们是 thenable。通常我只是通过 then 处理 thenable 函数的结果。然而,$q、mpromise 和 kew 库拥有相同的 API 用于创建、拒绝或通过 Promise。这里有 API 文档链接到每个库的引用部分。我通常不需要构造一个 Promise,除了本文中包装 Promise 的未知描述和 timeout 函数。请参考哪些我创建的 Promises。

Promise 库互操作

大多数 JavaScript Promise 库在 then 级别进行互操作。你可以从外部 Promise 创建一个 Promise,因为 Promise 可以包装任何类型的值。then 可以支持跨库工作。除了 then,其他 Promise 函数则可能不同。如果你需要你的库不包含的函数,你可以将一个基于你的库的 Promise 包装到一个新的、基于含有你所需函数的库创建的 Promise 里面。例如,JQuery 的 Promise 有时为人所诟病。那么你可以将其包装到 Q、$q、mpromise 或 kew 库的 Promise 中进行操作。 结语

现在我写了这篇文章,而一年前我却是犹豫要不要拥抱 Promise 的那个。我只是单纯地想完成一项工作。我不想学习新的 API,或是打破我原来的代码(因为误解了 Promise)。我曾经如此错误地认为!当我下了一点注时,就轻易赢得了可喜的成果。

在这篇文章中,我已经简单给出了一个单一 Promise、Promise 链和一个并行 Promise 的例子。Promises 不难使用。如果我可以使用它们,任何人都可以。要查看完整的概念,我支持你点击专家写的参考指南。从 Promises/A 的参考开始,从事实上的标准 JavaScript 的 Promise 开始。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章