本文概述
Promise是JavaScript开发界中的热门话题, 你一定应该熟悉它们。他们不容易缠住你的头。可能需要一些教程, 示例和大量实践来理解它们。
本教程的目的是帮助你理解JavaScriptPromise, 并推动你进一步练习使用它们。我将解释什么是诺言, 它们解决了哪些问题以及它们如何运作。本文描述的每个步骤都随附有一个jsbin代码示例, 以帮助你进行操作, 并用作进一步探索的基础。
什么是JavaScriptPromise?
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有什么性质?
一个Promise可以处于以下三种状态之一:未决, 已实现或被拒绝。创建后, Promise将处于待处理状态。从这里, 它可以进入已实现或已拒绝状态。我们称这种过渡为Promise的解决。Promise的已解决状态是其最终状态, 因此一旦实现或被拒绝, 它便会停留在该状态。
在rsvp.js中创建诺言的方法是通过所谓的揭示构造器。这种类型的构造函数只需要一个函数参数, 并立即使用两个参数满和拒绝调用它, 这可以将诺言转换为实现或拒绝状态:
var promise = new RSVP.Promise(function(fulfill, reject) {
(...)
});
这种JavaScript的Promise模式称为揭示构造器, 因为单个函数参数向构造函数提供了其功能, 但是确保了Promise的使用者不能操纵其状态。
Promise的使用者可以通过then方法添加其处理程序, 以对其状态更改做出反应。它需要实现和拒绝处理程序功能, 而这两个功能可能会丢失。
promise.then(onFulfilled, onRejected);
根据Promise解决过程的结果, 将异步调用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.
这个Promise教程演示了两件事。
首先, 在附加所有其他代码之后, 确实会调用我们附加到promise的处理程序。
其次, 仅在实现诺言时才调用实现处理程序, 并用其解决的价值(在本例中为掷骰子的结果)。拒绝处理程序也是如此。
规范Promise
该规范要求then函数(处理程序)也必须返回一个Promise, 这使得将Promise链接在一起, 导致代码看起来几乎是同步的:
signupPayingUser
.then(displayHoorayMessage)
.then(queueWelcomeEmail)
.then(queueHandwrittenPostcard)
.then(redirectToThankYouPage)
在这里, signupPayingUser返回一个promise, 并且promise链中的每个函数一旦完成就将使用前一个处理程序的返回值进行调用。出于所有实际目的, 这会序列化调用, 而不会阻塞主执行线程。
要查看如何通过链中上一项的返回值解决每个Promise, 我们返回掷骰子。我们希望将骰子最多掷三遍, 或者直到前六次出现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.
当折腾不为六时, tossASix返回的promise将被拒绝, 因此将使用实际的折腾调用拒绝处理程序。 logAndTossAgain在控制台上打印结果, 并返回表示另一个掷骰子的promise。反过来, 该折腾也被下一个logAndTossAgain拒绝并注销。
但是, 有时你会很幸运*, 并且设法获得六分:
Tossed a 4, need to try again.
Yay, managed to toss a 6.
*你不必那么幸运。如果掷三个骰子, 有至少42%的机会掷至少1个六。
这个例子也教给我们更多的东西。看到成功成功掷出六只后, 再怎么扔也不扔了?请注意, 除了最后一个logSuccess以外, 该链中的所有实现处理程序(到then的调用中的第一个参数)均为null。规范要求, 如果处理程序(实现或拒绝)不是函数, 则必须使用相同的值解析(实现或拒绝)返回的Promise。在上面的promise示例中, 实现处理程序null不是函数, 并且promise的值用6来实现。因此, then调用(链中的下一个)返回的promise也将被实现。其值为6。
重复执行直到出现实际的履行处理程序(即一个函数), 因此履行会逐渐下降直至得到处理。在我们的情况下, 这发生在链的尽头, 在该处, 它很高兴地注销到控制台。
处理错误
Promises / A +规范要求, 如果某个Promise被拒绝或在拒绝处理程序中引发错误, 则应由位于源头”下游”的拒绝处理程序进行处理。
利用以下trick流下降技术提供了一种处理错误的干净方法:
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返回值, 而第二个Promise是未定义的。然后, 下一个履行处理程序会炸毁, 尝试对此调用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);
我们熟悉上一个代码示例中的折腾。它只是创造了一个Promise, 而这个Promise总是可以通过掷骰子来实现的。我使用了RSVP.resolve, 这是一种便捷的方法, 可以用较少的仪式创建这样的Promise(请参见上面的代码中的[1])。
在threeDice中, 我创建了3个诺言, 每个诺言代表掷骰子, 最后将它们与RSVP.all组合在一起。 RSVP.all接受一组Promise, 并使用其已解析值的数组进行解析, 每个构成Promise对应一个Promise值, 同时保持其顺序。这意味着我们得到了结果的结果(请参见上面的代码[2]), 并且我们返回一个用它们的总和实现的Promise(请参见上面的代码[3])。
解决产生的Promise, 然后记录总数:
"Rolled 11 with three dice"
用Promise解决实际问题
JavaScriptPromise用于解决应用程序中的问题, 这些问题比异步无用原因掷骰子要复杂得多。
如果用滚动三个骰子代替将三个ajax请求发送到单独的端点, 并在所有端点都成功返回(或其中任何一个失败)后继续进行, 则你已经有一个有用的promises和RSVP.all应用程序。
如果正确使用, Promise会产生易于理解的代码, 比回调更容易推理, 因此更容易调试。由于它们已经成为规范的一部分, 因此无需建立有关错误处理的约定。
在本JavaScript教程中, 我们几乎没有涉及Promise的内容。 Promise库提供了许多方法和低级构造函数供你使用。掌握这些, 天空是你使用它们的极限。
关于作者
Balint Erdi很久以前是一位出色的角色扮演和AD&D粉丝, 并且现在很有希望, 现在是Ember.js粉丝。一直以来, 他对摇滚乐的热情。这就是为什么他决定在Ember.js上写一本书, 以摇滚作为书中应用程序的主题。在此处注册以了解启动时间。