JavaScript Promises:示例教程
已发表: 2022-03-11Promise 是 JavaScript 开发界的热门话题,您绝对应该熟悉它们。 它们不容易缠住你的头; 理解它们可能需要一些教程、示例和大量练习。
我编写本教程的目的是帮助您理解 JavaScript Promises,并推动您更多地练习使用它们。 我将解释什么是 Promise,它们解决了什么问题,以及它们是如何工作的。 本文中描述的每个步骤都附有一个jsbin
代码示例,以帮助您继续工作,并用作进一步探索的基础。
什么是 JavaScript 承诺?
Promise 是一种最终产生值的方法。 它可以被认为是 getter 函数的异步对应物。 其本质可以解释为:
promise.then(function(value) { // Do something with the 'value' });
Promise 可以代替回调的异步使用,并且它们提供了一些优于它们的好处。 随着越来越多的库和框架将它们作为处理异步的主要方式,它们开始获得支持。 Ember.js 就是这样一个框架的一个很好的例子。
有几个库实现了 Promises/A+ 规范。 我们将学习基本词汇,并通过一些 JavaScript Promise 示例以实用的方式介绍它们背后的概念。 我将在代码示例中使用更流行的实现库之一 rsvp.js。
准备好,我们会掷很多骰子!
获取 rsvp.js 库
Promise 和 rsvp.js 可以在服务器端和客户端使用。 要为nodejs安装它,请转到您的项目文件夹并键入:
npm install --save rsvp
如果你在前端工作并使用 bower,它只是一个
bower install -S rsvp
离开。
如果你只是想在游戏中正确,你可以通过简单的脚本标签包含它(使用jsbin
,你可以通过“添加库”下拉菜单添加它):
<script src="//cdn.jsdelivr.net/rsvp/3.0.6/rsvp.js"></script>
承诺有哪些属性?
Promise 可以处于以下三种状态之一: pending 、 fulfilled或denied 。 创建时,promise 处于挂起状态。 从这里,它可以进入已完成或已拒绝状态。 我们称这种转变为承诺的解决。 Promise 的已解决状态是它的最终状态,因此一旦它被履行或拒绝,它就会停留在那里。
在 rsvp.js 中创建 Promise 的方法是通过所谓的显示构造函数。 这种类型的构造函数接受一个函数参数并立即使用两个参数调用它, fulfill
和reject
,这可以将 promise 转换为已fulfilled
或已rejected
状态:
var promise = new RSVP.Promise(function(fulfill, reject) { (...) });
这种 JavaScript 承诺模式被称为揭示构造函数,因为单个函数参数向构造函数揭示了它的能力,但确保承诺的消费者不能操纵它的状态。
Promise 的消费者可以通过then
方法添加他们的处理程序来对其状态变化做出反应。 它需要一个实现和一个拒绝处理函数,这两者都可能丢失。
promise.then(onFulfilled, onRejected);
根据承诺的解决过程的结果, onFulfilled
或onRejected
处理程序被异步调用。
让我们看一个例子,它显示了事情的执行顺序:
function dieToss() { return Math.floor(Math.random() * 6) + 1; } console.log('1'); var promise = new RSVP.Promise(function(fulfill, reject) { var n = dieToss(); if (n === 6) { fulfill(n); } else { reject(n); } console.log('2'); }); promise.then(function(toss) { console.log('Yay, threw a ' + toss + '.'); }, function(toss) { console.log('Oh, noes, threw a ' + toss + '.'); }); console.log('3');
此代码段打印类似于以下内容的输出:
1 2 3 Oh, noes, threw a 4.
或者,如果我们幸运的话,我们会看到:
1 2 3 Yay, threw a 6.
这个 promises 教程演示了两件事。
首先,我们附加到 Promise 的处理程序确实是在所有其他代码运行后异步调用的。
其次,只有当 promise 被履行时才调用履行处理程序,并使用它解决的值(在我们的例子中,掷骰子的结果)。 拒绝处理程序也是如此。
链接承诺并向下渗透
规范要求then
函数(处理程序)也必须返回一个 Promise,这可以将 Promise 链接在一起,从而使代码看起来几乎是同步的:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage)
在这里, signupPayingUser
返回一个承诺,一旦完成,承诺链中的每个函数都会被调用,并使用前一个处理程序的返回值。 出于所有实际目的,这会在不阻塞主执行线程的情况下序列化调用。
为了查看每个承诺如何通过链中前一项的返回值得到解决,我们回到掷骰子。 我们最多要掷骰子 3 次,或者直到前 6 个出现 jsbin:
function dieToss() { return Math.floor(Math.random() * 6) + 1; } function tossASix() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; if (n === 6) { fulfill(n); } else { reject(n); } }); } function logAndTossAgain(toss) { console.log("Tossed a " + toss + ", need to try again."); return tossASix(); } function logSuccess(toss) { console.log("Yay, managed to toss a " + toss + "."); } function logFailure(toss) { console.log("Tossed a " + toss + ". Too bad, couldn't roll a six"); } tossASix() .then(null, logAndTossAgain) //Roll first time .then(null, logAndTossAgain) //Roll second time .then(logSuccess, logFailure); //Roll third and last time
当你运行这个 Promise 示例代码时,你会在控制台上看到类似这样的内容:
Tossed a 2, need to try again. Tossed a 1, need to try again. Tossed a 4. Too bad, couldn't roll a six.
当 toss 不是 6 时, tossASix
返回的 Promise 被拒绝,因此拒绝处理程序会使用实际的 toss 来调用。 logAndTossAgain
在控制台上打印结果并返回代表另一次掷骰子的承诺。 反过来,这种折腾也会被下一个logAndTossAgain
拒绝并注销。

但是,有时您会很幸运*,并设法掷出六:
Tossed a 4, need to try again. Yay, managed to toss a 6.
* 你不必那么幸运。 如果您掷三个骰子,则有约 42% 的机会掷出至少一个六。
这个例子也教会了我们更多的东西。 看看在第一次成功掷出 6 后,如何不再投掷? 请注意,链中的所有履行处理程序(对then
的调用中的第一个参数)都是null
,除了最后一个logSuccess
。 规范要求如果处理程序(履行或拒绝)不是函数,则返回的承诺必须以相同的值解决(履行或拒绝)。 在上面的 Promise 示例中,执行处理程序null
不是一个函数,并且 Promise 的值是用 6 完成的。所以then
调用(链中的下一个)返回的 Promise 也将被执行以 6 作为其值。
这一直重复,直到存在一个实际的履行处理程序(一个函数),所以履行会逐渐下降,直到它被处理。 在我们的例子中,这发生在链的末端,它被愉快地注销到控制台。
处理错误
Promises/A+ 规范要求,如果一个 Promise 被拒绝或在拒绝处理程序中抛出错误,它应该由源“下游”的拒绝处理程序处理。
利用下面的涓滴技术提供了一种处理错误的干净方法:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage) .then(null, displayAndSendErrorReport)
因为拒绝处理程序只添加在链的最末端,所以如果链中的任何履行处理程序被拒绝或抛出错误,它会慢慢下降,直到遇到displayAndSendErrorReport
。
让我们回到我们心爱的骰子,看看它的实际效果。 假设我们只想异步掷骰子并打印出结果:
var tossTable = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; function toss() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; fulfill(n); }); } function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUppercase() + "."); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain);
当你运行它时,什么也没有发生。 控制台上没有打印任何内容,似乎也没有抛出任何错误。
实际上,确实会抛出错误,我们只是看不到它,因为链中没有拒绝处理程序。 由于处理程序中的代码使用新堆栈异步执行,因此它甚至不会被注销到控制台。 让我们解决这个问题:
function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUpperCase() + "."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain) .then(null, logErrorMessage);
现在运行上面的代码确实会显示错误:
"Tossed a TWO." "Oops: Cannot read property 'toUpperCase' of undefined"
我们忘记从logAndTossAgain
返回一些东西,第二个承诺用undefined
实现。 然后,下一个履行处理程序在尝试调用toUpperCase
时崩溃了。 这是要记住的另一件重要的事情:总是从处理程序返回一些东西,或者在后续处理程序中准备好什么都没有传递。
建得更高
我们现在已经在本教程的示例代码中看到了 JavaScript Promise 的基础知识。 使用它们的一个很大的好处是,它们可以以简单的方式组合,以产生具有我们想要的行为的“复合”promise。 rsvp.js
库提供了其中的一些,您始终可以使用原语和这些更高级别的原语创建自己的。
对于最后一个最复杂的示例,我们将前往 AD&D 角色扮演的世界并掷骰子来获得角色分数。 这些分数是通过为角色的每个技能掷三个骰子来获得的。
让我先在这里粘贴代码,然后解释什么是新的:
function toss() { var n = Math.floor(Math.random() * 6) + 1; return new RSVP.resolve(n); // [1] } function threeDice() { var tosses = []; function add(x, y) { return x + y; } for (var i=0; i<3; i++) { tosses.push(toss()); } return RSVP.all(tosses).then(function(results) { // [2] return results.reduce(add); // [3] }); } function logResults(result) { console.log("Rolled " + result + " with three dice."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } threeDice() .then(logResults) .then(null, logErrorMessage);
我们从上一个代码示例中熟悉了toss
。 它只是创建了一个承诺,该承诺总是通过掷骰子的结果来实现。 我使用了RSVP.resolve
,这是一种方便的方法,可以用更少的仪式来创建这样的承诺(参见上面代码中的 [1])。
在threeDice
中,我创建了3 个promise,每个promise 代表一次掷骰子,最后将它们与RSVP.all
结合起来。 RSVP.all
接受一组承诺,并使用一组已解决的值来解决,每个组成承诺一个,同时保持它们的顺序。 这意味着我们在 results 中得到了折腾的results
(参见上面代码中的 [2]),并且我们返回了一个用它们的总和来实现的 promise(参见上面代码中的 [3])。
解析得到的 promise 然后记录总数:
"Rolled 11 with three dice"
使用 Promise 解决实际问题
JavaScript Promise 用于解决应用程序中的问题,这些问题远比异步掷骰子复杂得多。
如果您将掷三个骰子替换为向单独的端点发送三个 ajax 请求并在所有端点都成功返回(或者如果其中任何一个都失败)时继续,那么您已经有了一个有用的 promise 和RSVP.all
应用程序。
如果使用得当,Promise 会生成易于理解的代码,因此比回调更容易调试。 无需设置有关例如错误处理的约定,因为它们已经是规范的一部分。
在本 JavaScript 教程中,我们几乎没有触及到 Promise 可以做什么的皮毛。 Promise 库提供了许多可供您使用的方法和低级构造函数。 掌握这些,天空就是你可以用它们做的极限。
关于作者
Balint Erdi 很久以前是一个伟大的角色扮演和 AD&D 粉丝,现在是一个伟大的承诺和 Ember.js 粉丝。 一直不变的是他对摇滚乐的热情。 这就是为什么他决定写一本关于 Ember.js 的书,以摇滚乐作为书中应用程序的主题。 在此处注册以了解它何时启动。