JavaScript Promise 规范解析与实战应用

作者:拾光时间:2026-10-28 13:17:55浏览:277

本文系统解析 JavaScript Promise 异步编程模型,对比传统 Callback 模式的缺陷,详细阐述 Promise/A+ 规范中 pending、fulfilled 和 rejected 三种状态及 then 方法的核心逻辑。文章包含手动实现 Promise 的思路与代码示例,并对比 jQuery、when.js 等主流库的 Promise 实现差异,旨在帮助开发者掌握现代 JS 异步处理的最佳实践。

本文旨在介绍 Promise 的基本用法。

长期以来,JavaScript 处理异步操作主要依赖回调函数(callback)。在前端开发领域,这种机制已根深蒂固。无论是浏览器厂商、SDK 开发商还是各类库的作者,在设计 API 时几乎都遵循这一模式。

近年来,随着 JavaScript 开发模式的成熟,CommonJS 规范应运而生,其中包含了 Promise 规范。Promise 彻底改变了 JavaScript 异步编程的写法,使其更加直观易懂。

在传统的回调模型中,若需执行一系列异步任务,代码可能如下所示:


loadImg('a.jpg', function() {
  loadImg('b.jpg', function() {
    loadImg('c.jpg', function() {
      console.log('all done!');
    });
  });
});

这即所谓的“回调地狱”。当异步任务繁多时,维护大量嵌套的回调函数将是一场灾难。随着 Node.js 的流行,许多团队尝试使用它,但面对层层嵌套的回调,往往望而却步。

闲话少说,下面进入正题。

Promise 并不陌生,该规范已存在一段时间,并被纳入 ES6 标准。高版本的 Chrome 和 Firefox 浏览器已原生支持 Promise,尽管其 API 较流行的第三方类库略少。

从字面理解,“Promise”意为“承诺”。当 A 调用 B 时,B 返回一个“承诺”给 A。A 可以据此规划:若 B 成功返回结果,则执行方案 S1;若 B 失败,则执行应急方案 S2。这样,潜在风险便处于 A 的可控范围内。

上述逻辑对应的代码大致如下:


var resB = B();
var runA = function() {
  resB.then(execS1, execS2);
};
runA();

单看这行代码似乎平淡无奇,但现实场景往往更复杂。A 完成任务可能需要依赖多个 B 的响应,甚至需同时向多方询问,待所有应答返回后再执行下一步。最终代码可能如下:


var resB = B();
var resC = C();
...

var runA = function() {
  reqB
    .then(resC, execS2)
    .then(resD, execS3)
    .then(resE, execS4)
    ...
    .then(execS1);
};

runA();

在此示例中,每个被询问者在反馈不符合预期时采用了不同的处理机制。实际上,Promise 规范并未强制要求如此,开发者甚至可以不做任何处理(即不传入 then 的第二个参数)或统一处理。

接下来,我们了解 Promise/A+ 规范:

  • 一个 Promise 对象可能处于三种状态之一:等待(pending)、已完成(fulfilled)或已拒绝(rejected)。
  • Promise 的状态只能从“等待”转换为“完成”或“拒绝”,且不可逆向转换;“完成”与“拒绝”状态之间也不能相互转换。
  • Promise 必须实现 then 方法(这是 Promise 的核心),且 then 必须返回一个 Promise。同一 Promise 可多次调用 then,且回调的执行顺序与定义顺序一致。
  • then 方法接受两个参数:第一个是成功时的回调,在 Promise 从“等待”转为“完成”时调用;第二个是失败时的回调,在 Promise 从“等待”转为“拒绝”时调用。此外,then 还可接受另一个 Promise 或“类 then”对象(即 thenable 对象)。

可以看到,Promise 规范的内容并不复杂,读者可以尝试自行实现。

以下是笔者参考多个类 Promise 库后简单实现的一个版本,完整代码请参见 promiseA。

简单分析其实现思路:

Promise 构造函数接受一个函数 resolver,可视为传入一个异步任务。resolver 接受两个参数:成功回调和失败回调,这两个参数与通过 then 传入的参数是对等的。

其次是 then 的实现。由于 Promise 要求 then 必须返回一个 Promise,因此在调用 then 时会生成一个新的 Promise,并挂载在当前 Promise 的 _next 属性上。对同一 Promise 多次调用 then,均会返回之前生成的 _next。

由于 then 方法接受的两个参数均为可选,且类型不限,可以是函数、具体值或另一个 Promise。以下是 then 的具体实现:


Promise.prototype.then = function(resolve, reject) {
  var next = this._next || (this._next = Promise());
  var status = this.status;
  var x;

  if('pending' === status) {
    isFn(resolve) && this._resolves.push(resolve);
    isFn(reject) && this._rejects.push(reject);
    return next;
  }

  if('resolved' === status) {
    if(!isFn(resolve)) {
      next.resolve(resolve);
    } else {
      try {
        x = resolve(this.value);
        resolveX(next, x);
      } catch(e) {
        this.reject(e);
      }
    }
    return next;
  }

  if('rejected' === status) {
    if(!isFn(reject)) {
      next.reject(reject);
    } else {
      try {
        x = reject(this.reason);
        resolveX(next, x);
      } catch(e) {
        this.reject(e);
      }
    }
    return next;
  }
};

 

此处对 then 进行了简化,其他 Promise 类库的实现更为复杂且功能更多,例如包含第三个参数——notify,用于表示 Promise 的当前进度,这在文件上传等场景中非常有用。处理 then 的各种参数是最复杂的部分,有兴趣的读者可参考其他类 Promise 库的实现。

在 then 的基础上,还需要至少两个方法,分别用于将 Promise 状态从 pending 转换为 resolved 或 rejected,并执行相应的回调队列,即 resolve() 和 reject() 方法。

至此,一个简单的 Promise 设计完成。下面简单实现两个 Promise 化的函数:


function sleep(ms) {
  return function(v) {
    var p = Promise();

    setTimeout(function() {
      p.resolve(v);
    }, ms);

    return p;
  };
};

function getImg(url) {
  var p = Promise();
  var img = new Image();

  img.onload = function() {
    p.resolve(this);
  };

  img.onerror = function(err) {
    p.reject(err);
  };

  img.url = url;

  return p;
};

由于 Promise 构造函数接受异步任务作为参数,因此 getImg 还可以这样调用:


function getImg(url) {
  return Promise(function(resolve, reject) {
    var img = new Image();

    img.onload = function() {
      resolve(this);
    };

    img.onerror = function(err) {
      reject(err);
    };

    img.url = url;
  });
};

接下来(见证奇迹的时刻),假设有一个复杂需求:异步获取 JSON 配置,解析数据获取图片列表,然后按顺序队列加载图片,每张图片加载时显示 loading 效果。


function addImg(img) {
  $('#list').find('> li:last-child').html('').append(img);
};

function prepend() {
  $('<li>')
    .html('loading...')
    .appendTo($('#list'));
};

function run() {
  $('#done').hide();
  getData('map.json')
    .then(function(data) {
      $('h4').html(data.name);

      return data.list.reduce(function(promise, item) {
        return promise
          .then(prepend)
          .then(sleep(1000))
          .then(function() {
            return getImg(item.url);
          })
          .then(addImg);
      }, Promise.resolve());
    })
    .then(sleep(300))
    .then(function() {
      $('#done').show();
    });
};

$('#run').on('click', run);

这里的 sleep 仅为展示效果,可点击查看 demo!Node.js 示例可查看此处。

在此,Promise.resolve(v) 静态方法简单返回一个以 v 为肯定结果的 Promise。v 可不传入,也可以是函数或包含 then 方法的对象或函数(即 thenable)。

类似的静态方法还有 Promise.cast(promise),生成一个以 Promise 为肯定结果的 Promise;

Promise.reject(reason),生成一个以 reason 为否定结果的 Promise。

实际使用场景往往复杂,常需多个异步任务穿插执行,并行或串行共存。此时可对 Promise 进行扩展,例如实现 Promise.all(),接受 Promises 队列并等待它们全部完成后再继续;或实现 Promise.any(),当 Promises 队列中任意一个处于完成态时即触发下一步操作。

标准 Promise

可参考 html5rocks 的文章《JavaScript Promises》。目前 Chrome、Firefox 等高级浏览器已内置 Promise 对象,提供更多操作接口,例如 Promise.all(),支持传入 Promises 数组,当所有 Promise 完成时执行 then。此外还有更友好强大的异常捕获功能,应对日常异步编程已足够。

第三方库的 Promise

现今流行的各大 JS 库几乎都不同程度地实现了 Promise,如 dojo、jQuery、Zepto、when.js、Q 等,但暴露出的多为 Deferred 对象。以 jQuery(Zepto 类似)为例,实现上述 getImg():


function getImg(url) {
  var def = $.Deferred();
  var img = new Image();

  img.onload = function() {
    def.resolve(this);
  };

  img.onerror = function(err) {
    def.reject(err);
  };

  img.src = url;

  return def.promise();
};

当然,jQuery 中许多操作返回的是 Deferred 或 Promise,如 animate、ajax:


// animate
$('.box')
  .animate({'opacity': 0}, 1000)
  .promise()
  .then(function() {
    console.log('done');
  });

// ajax
$.ajax(options).then(success, fail);
$.ajax(options).done(success).fail(fail);

// ajax queue
$.when($.ajax(options1), $.ajax(options2))
  .then(function() {
    console.log('all done.');
  }, function() {
    console.error('There something wrong.');
  });

jQuery 还实现了 done() 和 fail() 方法,它们实际上是 then 方法的快捷方式。

处理 Promises 队列时,jQuery 实现的是 $.when() 方法,用法与 Promise.all() 类似。

其他类库中,值得一提的是 when.js。其代码量不多,完整实现了 Promise,同时支持 Browser 和 Node.js,并提供更丰富的 API,是不错的选择。限于篇幅,此处不再展开。

尾声

我们看到,无论 Promise 实现多么复杂,其用法却很简单,代码组织清晰,从此不再受 Callback 的折磨。

最后,Promise 如此优雅!但它仅解决了回调深层嵌套的问题,真正简化 JavaScript 异步编程的还是 Generator。在 Node.js 端,建议考虑使用 Generator。

下一篇,我们将研究 Generator。

github原文: https://github.com/chemdemo/chemdemo.github.io/issues/6

作者最新文章