JavaScript Promise:简介

Promise 可简化延迟和异步计算。promise 表示尚未完成的操作。

Jake Archibald
Jake Archibald

各位开发者,请做好准备,迎接 AI 史上的关键时刻 Web 开发。

[击鼓开始]

Promise 现已支持 JavaScript!

[烟花绽放,彩纸飘落,人群如火如荼]

此时,您属于以下某个类别:

  • 人们在你周围欢呼,但你感到很不清楚 。也许您甚至不确定一个“promise”。你可能会耸肩,但是 闪闪发光的纸张重重了。如果是,请不要 担心这个问题,我花了很长的时间才明白为什么应该关注这个问题 等。您可能希望从头开始。
  • 您非常抓狂!还差一点点,对吧?您之前使用过这些 Promise 内容 但让您困扰的是,所有实现的 API 都略有不同。 官方 JavaScript 版本的 API 是什么?您可能希望开始 及其术语
  • 你早就知道这些,你会嘲笑那些跳楼子的人 对他们来说就像是新闻一样不妨花点时间自豪一把, 然后直接前往 API 参考文档

浏览器支持和 polyfill

浏览器支持

  • Chrome:32。 <ph type="x-smartling-placeholder">
  • Edge:12。 <ph type="x-smartling-placeholder">
  • Firefox:29。 <ph type="x-smartling-placeholder">
  • Safari:8. <ph type="x-smartling-placeholder">

来源

使缺少完整 promise 实现的浏览器符合规范 或者向其他浏览器和 Node.js 添加 promise,请查看 polyfill (Gzip 压缩大小为 2k)。

人们究竟为何欢呼雀跃?

JavaScript 是单线程的,这意味着两段脚本无法在 它们必须一个接一个地运行在浏览器中,JavaScript 与浏览器、Google+ 或 。但通常情况下,JavaScript 与绘制、更新 样式和处理用户操作(例如,突出显示文字和 使用表单控件)。在其中一个方面进行活动会延迟其他方面。

作为人类,您是多线程的。您可以使用多个手指输入内容, 你可以一边开车一边交谈唯一的屏蔽操作 我们必须处理的功能就是打喷嚏,此时所有当前活动都必须 在打喷嚏期间会暂停显示这真是太烦了 尤其是当您在开车并想与人交谈时。你不会 都想编写像打喷嚏的代码

您可能使用事件和回调来绕过此问题。活动如下:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

这根本不是打喷嚏。我们获取图片,添加几个监听器,然后 JavaScript 可以停止执行,直到调用其中一个监听器。

遗憾的是,在上面的示例中, 所以我们需要使用 “complete”(完成)属性:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

这无法捕获在我们有机会监听之前出错的图像 them;但遗憾的是,DOM 也没有给出解决之道。此外,这 加载一张图片。如果我们需要知道 的图片已加载。

活动并不总是最佳方式

事件非常适合于在同一时间发生多次的事情 对象 - keyuptouchstart 等。对于这些事件,您并不关注 在添加监听器之前发生的情况。但当谈到 异步成功/失败,最好是这样:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

这是 promise 执行的操作,但命名方式更好。如果 HTML 图片元素的 “ready”方法,我们可以执行以下操作:

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

从最基本的层面来说,promise 有点类似于事件监听器,不过:

  • promise 只能成功或失败一次。它不能成功或失败两次 也无法从成功转为失败或从失败转为成功。
  • 如果 promise 已成功或失败,并且之后您添加了成功/失败 回调函数时,系统会调用正确的回调函数,即使事件发生在 位置。

这对于异步成功/失败非常有用 对某些产品推出的确切时间感兴趣,并且对产品更感兴趣 以及对结果的反应

Promise 术语

Domenic Denicola 校对初稿 并给我打分了“F”他把我关押了, 强迫我把 状态和结果 看了 100 遍,我给爸妈写了封信。尽管如此,我仍然 混淆了很多术语,以下是一些基本概念:

promise 可以是:

  • 已执行 - 与 promise 相关的操作已成功执行
  • rejected - 与 promise 相关的操作失败
  • 待定 - 尚未执行或拒绝
  • settled - 已履行或遭拒

规范 还使用了术语 thenable 来描述类似于 promise 的对象, 因为它具有 then 方法。这个词让我想起了前英格兰足球队 管理员 Terry Venables, 我会尽可能少用它。

Promise 在 JavaScript 中受支持!

Promise 有一段时间以库的形式出现,例如:

上述代码与 JavaScript promise 具有共同的标准化行为 名为 Promise/A+。如果 您是 jQuery 用户,他们有 推迟。不过, 推迟项不符合 Promise/A+ 要求,因此 存在细微差别且不太实用 请多加注意jQuery 还有 Promise 类型,但这只是 Deferred 的子集,存在同样的问题。

虽然 promise 实现遵循标准化行为, 会有很大差异JavaScript promise 在 API 中类似于 RSVP.js。 创建 promise 的方法如下:

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

promise 构造函数接受一个参数、一个包含两个参数的回调。 解决并拒绝。在回调中执行一些操作(例如异步),然后调用 如果一切正常,则解决,否则调用拒绝。

与普通旧版 JavaScript 中的 throw 一样,通常的做法是 并返回一个 Error 对象。Error 对象的优势在于 堆栈轨迹,使调试工具更加实用。

下面展示了如何使用该 promise:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then() 接受两个参数:一个用于成功情形的回调,以及另一个 测试失败情况两者都是可选的,因此您可以为 只针对成功或失败情形。

JavaScript promise 最初是在 DOM 中出现并称为“Futures”,之后重命名为“Promises”, 最后移到了 JavaScript 中使用 JavaScript 而不是 DOM 非常棒,因为它们可以在非浏览器 JS 环境中使用,例如 Node.js(他们是否在核心 API 中使用它们是另一个问题)。

尽管它们是 JavaScript 的一项功能,但 DOM 也能使用。在 事实上,采用异步成功/失败方法的所有新 DOM API 都将使用 promise。 Google Cloud 配额管理字体加载事件 ServiceWorkerWeb MIDI信息流等。

与其他库的兼容性

JavaScript promise API 会将任何使用 then() 方法的内容视为 类似于 promise 的(或者用 promise 中的 thenable 叹息),因此,如果使用库 返回 Q promise 的文本没有问题, JavaScript promise。

不过,正如我之前提到的,jQuery 的 Deferred 有点...没有用。 幸运的是,您可以将其转为标准 promise,这很值得 :

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

在这里,jQuery 的 $.ajax 返回一个 Deferred。由于它使用 then() 方法, Promise.resolve() 可以将其转换为 JavaScript promise。不过, 有时 deferred 会将多个参数传递给其回调,例如:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

而 JS promise 会忽略除第一个之外的所有参数:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

幸好,通常这就是您想要的,或者至少为您提供了 自己想要的内容另请注意,jQuery 并不遵循 将 Error 对象传入遭拒中。

简化复杂的异步代码

好吧,我们来编写一些代码。假设我们想要:

  1. 启动一个旋转图标来指示正在加载
  2. 获取故事的一些 JSON,以便我们确定每个章节的标题和网址
  3. 为页面添加标题
  4. 获取每个章节
  5. 将故事添加到页面中
  6. 停止旋转图标

...但如果操作过程中出现问题,也要告知用户。我们需要 还要在那一点停止转环,否则,它就会不停地旋转, 眩晕并崩溃到其他界面

当然,您不会使用 JavaScript 来讲述故事, 以 HTML 格式投放的速度较快 但在处理 API 时,这种模式很常见: 抓取,然后在全部完成后执行一些操作

首先,我们来了解如何从网络中提取数据:

对 XMLHttpRequest 执行 promise

旧 API 将更新为使用 promise(如果可以向后兼容) 兼容。XMLHttpRequest是主要候选人,但目前 让我们编写一个简单的函数来发出 GET 请求:

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

现在,让我们来使用它:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

现在,我们无需手动输入 XMLHttpRequest 即可发出 HTTP 请求,太棒了, 少看到XMLHttpRequest令人讨厌的驼峰,我的生活会越幸福。

then() 不是故事的结尾,您可以将 then 链接在一起, 转换值或逐个运行其他异步操作。

转换值

只需返回新值即可转换值:

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

作为一个实际的例子,让我们回到:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

响应是 JSON,但我们当前收到的是纯文本。周三 可以更改我们的 get 函数以使用 JSON responseType、 但我们也可以通过 promise 来解决这个问题:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

由于 JSON.parse() 接受单个参数并返回转换后的值, 我们可以创建一种捷径:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

实际上,我们可以非常轻松地构建 getJSON() 函数:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON() 仍会返回一个 promise,该 promise 会获取网址并解析 以 JSON 格式返回响应

将异步操作加入队列

您还可以链接多个 then,以便按顺序运行异步操作。

then() 回调返回某些内容时,这有点儿神奇。 如果返回一个值,系统会使用该值调用下一个 then()。不过, 如果您返回类似于 promise 的内容,则下一个 then() 会等待, 仅在 promise 产生结果(成功/失败)时调用。例如:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

现在,我们向 story.json 发出异步请求,从而获得一组 要请求的网址,我们会请求获取第一个。此时 promise 从简单的回调模式中脱颖而出。

您甚至可以创建一种快捷方法来获取章节:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

getChapter 被调用之前,我们不会下载 story.json,但下一个 调用 getChapter 后,我们会重复使用故事 promise,因此 story.json 仅提取一次。太棒了!

错误处理

如前所述,then() 有两个参数:一个表示成功,一个表示成功 表示失败(按照 promise 中的说法,即执行和拒绝):

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

您还可以使用 catch()

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

catch() 没什么特别之处,就是糖的 then(undefined, func),但更易于阅读。请注意,这两个代码 以上示例的行为有所不同,后者等同于:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

差别很小,但非常有用。Promise 拒绝次数跳过 转发到下一个 then(),并带有拒绝回调(或 catch(),因为 (等效)。采用then(func1, func2)后,func1func2将 不能同时调用两者。但如果使用 then(func1).catch(func2),两者都将: 在 func1 拒绝时调用,因为它们是链中的单独步骤。乘坐 以下:

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

上述流程与常规的 JavaScript try/catch 非常相似, 发生于一次“尝试”中立即转到 catch() 代码块。以下是 看上去像是流程图一样(因为我喜欢流程图):

蓝线表示执行的 promise 路径,红路表示执行的 promise 路径 拒绝。

JavaScript 异常和 promise

当 promise 被明确拒绝(但也被隐式拒绝)时,就会发生拒绝 如果在构造函数回调中抛出错误:

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

这意味着,在 promise 构造函数回调,以便自动捕获错误并 被拒。

对于在 then() 回调中抛出的错误也是如此。

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

错误处理实践

在我们的故事和章节中,我们可以使用 catch 向用户显示错误:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

如果提取 story.chapterUrls[0] 失败(例如,http 500 或用户处于离线状态), 它将跳过所有后续成功回调,包括 getJSON(),它会尝试将响应解析为 JSON,并跳过 用于向页面添加 chapter1.html 的回调。而是直接抓住机会 回调。导致出现“Failed to show chapter”错误消息将被添加到 前任何操作都失败了。

与 JavaScript 的 try/catch 一样,系统会捕获错误,而后续代码 所以旋转图标始终处于隐藏状态,这正是我们想要的。通过 会成为以下内容的非阻塞异步版本:

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

您可能想要 catch() 仅用于记录目的,而不恢复 错误。为此,只需重新抛出错误即可。我们可以 getJSON() 方法:

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

因此,我们已成功获取了一个章节,但我们需要所有章节。让我们一起来 情况。

并行式和顺序式:两者兼得

异步并不容易。如果你觉得很难上路 尝试像编写同步代码那样编写代码。在此示例中:

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'

可以!但这是同步的,而且在内容下载时浏览器会锁定。接收者 使其异步,我们使用 then() 来依次执行任务。

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch &amp; display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

但是,我们如何循环遍历章节网址并按顺序获取呢?这个 无效

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach 不具有异步感知能力,因此章节将以任意顺序显示 这就是《Pulp 小说》的写作过程。这不是 《低俗小说》,让我们解决这个问题。

创建序列

我们希望将 chapterUrls 数组转换为 promise 序列。我们可以使用 then() 实现此目的:

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

这是我们第一次看到 Promise.resolve(),它会创建一个 promise 可解析为您指定的任意值。如果您向它传递 Promise 的实例,则会返回(注意:这是一个 对某些实现尚未遵循的规范的更改)。如果您 将类似于 promise 的内容传递给它(具有 then() 方法),它会创建一个 以同样的方式执行/拒绝的真实 Promise。如果您通过 任何其他值,例如Promise.resolve('Hello'),它会创建一个 promise。如果您调用它时没有指定价值 如上所示,它在执行时将返回“undefined”。

此外,还有 Promise.reject(val),它会创建一个 promise 以拒绝并返回 您为它指定的值(或“undefined”)。

我们可以使用 array.reduce:

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

这与上一个示例执行的操作相同,但不需要单独的 “sequence”变量。系统会为数组中的每个项调用缩减回调。 “sequence”第一次是 Promise.resolve(),但是对于 调用“sequence”是从上一次调用返回的值。array.reduce 非常适合将数组浓缩为单个值,在本例中 是一个 promise。

我们来总结一下:

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

现在我们已经实现,即同步版本的完全异步版本。但我们能 。此时,我们的页面正在下载,如下所示:

浏览器就很擅长一次下载多项内容 逐个下载章节,从而提升性能。我们想要做的是 然后全部下载完,再进行处理 幸运的是,它有一个 API:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all 接受一个 promise 数组,并创建一个执行的 promise 所有操作都已成功完成您获得了一系列结果 promise 执行的顺序)。

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

根据连接情况,这可能比逐个加载要快几秒钟, 而且比我们第一次尝试的代码要少这些章节可下载为 但它们在屏幕上以正确的顺序显示。

不过,我们仍然可以提升感知性能。第一章下载完后, 应该将其添加到页面。这样,用户就能在阅读其余内容之前开始阅读 已经有新篇章了第三章下载完后,我们不会将其添加到 因为用户可能不知道缺少第二章。第二章 可以添加第二章和第三章,以此类推

为此,我们同时获取所有章节的 JSON,然后创建一个 按顺序将它们添加到文档中:

getJSON('story.json')
.then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence
      .then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

好了,两全其美!且投放时间相同 但用户能更快看到第一点内容。

在这个小示例中,所有章节几乎同时下载完毕, 一次展示一条信息的好处就会越大, 章节。

使用 Node.js 样式的回调或 大约有 但更重要的是,操作起来并不容易。不过, 与其他 ES6 功能结合使用时,promise 功能更加完善 就能更轻松地访问

额外奖励:扩展功能

自从我最初撰写本文以来,使用 Promise 的功能已经扩展 。从 Chrome 55 开始,异步函数允许基于 promise 的代码 像同步一样编写,但不会阻塞主线程。您可以 如需了解详情,请参阅我的异步函数一文。还有 在主流浏览器中同时广泛支持 Promise 和异步函数。 有关详情,请参见 MDN 的 承诺异步 函数 参考。

感谢 Anne van Kesteren、Domenic Denicola、Tom Ashworth、Remy Sharp、 Addy Osmani、Arthur Evans 和 Yutaka Hirano 负责制作 纠正/建议。

此外,感谢 Mathias Bynens更新各个部分 部分。