想象一下,你是一位顶级歌手,粉丝们日夜催问你的新歌。
为了得到一些喘息,你承诺在歌曲发布时将它发送给他们。你给粉丝们一个列表。他们可以填写他们的电子邮件地址,以便在歌曲可用时,所有订阅者都能立即收到它。即使出了什么大问题,比如工作室失火,导致你无法发布歌曲,他们仍会收到通知。
每个人都很开心:你,因为人们不再围着你,粉丝们,因为他们不会错过这首歌。
这是我们经常在编程中遇到的事物的真实类比
- 一个“生成代码”做一些事情并花费时间。例如,一些通过网络加载数据的代码。那是一个“歌手”。
- 一个“消费代码”想要在“生成代码”准备好后得到它的结果。许多函数可能需要该结果。这些是“粉丝”。
- Promise 是一个特殊的 JavaScript 对象,它将“生成代码”和“消费代码”链接在一起。就我们的类比而言:这是“订阅列表”。“生成代码”需要任何时间来生成承诺的结果,“promise”在准备好后向所有订阅的代码提供该结果。
这个类比并不是非常准确,因为 JavaScript promise 比简单的订阅列表更复杂:它们具有附加的功能和限制。但一开始这样就可以了。
promise 对象的构造函数语法是
let promise = new Promise(function(resolve, reject) {
// executor (the producing code, "singer")
});
传递给 new Promise
的函数称为executor。当创建 new Promise
时,executor 会自动运行。它包含最终应该生成结果的生成代码。就上面的类比而言:executor 是“歌手”。
它的参数 resolve
和 reject
是 JavaScript 本身提供的回调。我们的代码只在 executor 中。
无论 executor 何时获得结果,无论是早还是晚,它都应该调用其中一个回调
resolve(value)
— 如果作业成功完成,结果为value
。reject(error)
— 如果发生错误,error
是错误对象。
因此,总结一下:executor 自动运行并尝试执行作业。当它完成尝试时,如果成功,它会调用 resolve
,如果出错,它会调用 reject
。
new Promise
构造函数返回的 promise
对象具有以下内部属性
state
— 最初为"pending"
,然后在调用resolve
时更改为"fulfilled"
,或在调用reject
时更改为"rejected"
。result
— 最初为undefined
,然后在调用resolve(value)
时更改为value
,或在调用reject(error)
时更改为error
。
因此,executor 最终将 promise
移至其中一个状态
稍后我们将了解“粉丝”如何订阅这些更改。
以下是一个 Promise 构造函数和一个简单的执行器函数的示例,其中包含需要时间(通过 setTimeout
)的“生成代码”
let promise = new Promise(function(resolve, reject) {
// the function is executed automatically when the promise is constructed
// after 1 second signal that the job is done with the result "done"
setTimeout(() => resolve("done"), 1000);
});
通过运行以上代码,我们可以了解两件事
-
执行器会自动立即被调用(通过
new Promise
)。 -
执行器接收两个参数:
resolve
和reject
。这些函数由 JavaScript 引擎预先定义,因此我们无需创建它们。我们只应在准备就绪时调用其中一个函数。经过一秒钟的“处理”后,执行器调用
resolve("done")
以生成结果。这会更改promise
对象的状态
这是一个作业成功完成的示例,即“已完成的 Promise”。
现在来看一个执行器用错误拒绝 Promise 的示例
let promise = new Promise(function(resolve, reject) {
// after 1 second signal that the job is finished with an error
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
调用 reject(...)
会将 Promise 对象移至“已拒绝”状态
总而言之,执行器应执行一项作业(通常是需要花费时间的事情),然后调用 resolve
或 reject
来更改相应 Promise 对象的状态。
已解决或已拒绝的 Promise 被称为“已解决”,与最初的“待处理”Promise 相对。
执行器应仅调用一个 resolve
或一个 reject
。任何状态更改都是最终的。
将忽略所有进一步的 resolve
和 reject
调用
let promise = new Promise(function(resolve, reject) {
resolve("done");
reject(new Error("…")); // ignored
setTimeout(() => resolve("…")); // ignored
});
这个想法是,执行器完成的作业可能只有一个结果或一个错误。
此外,resolve
/reject
仅期望一个参数(或没有参数),并将忽略其他参数。
Error
对象拒绝如果出现问题,执行器应调用 reject
。可以使用任何类型的参数来完成此操作(就像 resolve
一样)。但建议使用 Error
对象(或从 Error
继承的对象)。其原因很快就会变得明显。
resolve
/reject
在实践中,执行器通常会异步执行某些操作,并在一段时间后调用 resolve
/reject
,但并非必须如此。我们还可以像这样立即调用 resolve
或 reject
let promise = new Promise(function(resolve, reject) {
// not taking our time to do the job
resolve(123); // immediately give the result: 123
});
例如,当我们开始执行一项作业,但随后发现所有内容都已完成并已缓存时,可能会发生这种情况。
这很好。我们立即获得一个已解决的 Promise。
state
和 result
是内部的Promise 对象的 state
和 result
属性是内部的。我们无法直接访问它们。我们可以使用 .then
/.catch
/.finally
方法来实现此目的。它们在下面进行了描述。
使用者:then、catch
Promise 对象充当执行器(“生成代码”或“歌手”)和使用者函数(“粉丝”)之间的链接,后者将收到结果或错误。可以使用 .then
和 .catch
方法注册(订阅)使用者函数。
then
最重要的基本方法是 .then
。
语法为
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);
.then
的第一个参数是一个函数,该函数在 Promise 已解决并收到结果时运行。
.then
的第二个参数是一个函数,当 Promise 被拒绝并收到错误时运行。
例如,以下是对成功解决的 Promise 的反应
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
// resolve runs the first function in .then
promise.then(
result => alert(result), // shows "done!" after 1 second
error => alert(error) // doesn't run
);
第一个函数已执行。
在拒绝的情况下,第二个函数
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// reject runs the second function in .then
promise.then(
result => alert(result), // doesn't run
error => alert(error) // shows "Error: Whoops!" after 1 second
);
如果我们只对成功完成感兴趣,那么我们可以只向 .then
提供一个函数参数
let promise = new Promise(resolve => {
setTimeout(() => resolve("done!"), 1000);
});
promise.then(alert); // shows "done!" after 1 second
catch
如果我们只对错误感兴趣,那么我们可以使用 null
作为第一个参数:.then(null, errorHandlingFunction)
。或者我们可以使用 .catch(errorHandlingFunction)
,它完全相同
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second
调用 .catch(f)
是 .then(null, f)
的完全类似,它只是一个简写。
清理:finally
就像在常规 try {...} catch {...}
中有 finally
子句一样,在 Promise 中也有 finally
。
调用 .finally(f)
类似于 .then(f, f)
,因为当 Promise 解决时,f
总会运行:无论它解决还是拒绝。
finally
的目的是在之前的操作完成后设置一个处理程序来执行清理/完成。
例如,停止加载指示器,关闭不再需要的连接等。
把它想象成一个派对结束者。无论派对好坏,有多少朋友参加,我们仍然需要(或至少应该)在派对结束后进行清理。
代码可能如下所示
new Promise((resolve, reject) => {
/* do something that takes time, and then call resolve or maybe reject */
})
// runs when the promise is settled, doesn't matter successfully or not
.finally(() => stop loading indicator)
// so the loading indicator is always stopped before we go on
.then(result => show result, err => show error)
请注意,finally(f)
并不完全是 then(f,f)
的别名。
有重要的区别
-
finally
处理程序没有参数。在finally
中,我们不知道 Promise 是否成功。这没关系,因为我们的任务通常是执行“常规”完成程序。请看上面的示例:如您所见,
finally
处理程序没有参数,并且 Promise 结果由下一个处理程序处理。 -
finally
处理程序将结果或错误“传递”给下一个合适的处理程序。例如,这里结果通过
finally
传递到then
new Promise((resolve, reject) => { setTimeout(() => resolve("value"), 2000); }) .finally(() => alert("Promise ready")) // triggers first .then(result => alert(result)); // <-- .then shows "value"
如您所见,第一个 Promise 返回的
value
通过finally
传递到下一个then
。这非常方便,因为
finally
不是用来处理 Promise 结果的。如前所述,无论结果如何,它都是执行通用清理的地方。以下是一个错误的示例,以便我们了解它是如何通过
finally
传递到catch
的new Promise((resolve, reject) => { throw new Error("error"); }) .finally(() => alert("Promise ready")) // triggers first .catch(err => alert(err)); // <-- .catch shows the error
-
finally
处理程序也不应该返回任何内容。如果返回,则会忽略返回的值。此规则的唯一例外是当
finally
处理程序抛出错误时。然后,此错误将转到下一个处理程序,而不是任何先前的结果。
总结
finally
处理程序不会获取前一个处理程序的结果(它没有参数)。此结果将传递给下一个合适的处理程序。- 如果
finally
处理程序返回内容,则会被忽略。 - 当
finally
抛出错误时,执行将转到最近的错误处理程序。
如果我们按照预期的方式使用 finally
(用于通用清理过程),这些功能非常有用,可以正确地完成工作。
如果 Promise 处于待处理状态,.then/catch/finally
处理程序将等待其结果。
有时,当我们向 Promise 添加处理程序时,它可能已经解决。
在这种情况下,这些处理程序会立即运行
// the promise becomes resolved immediately upon creation
let promise = new Promise(resolve => resolve("done!"));
promise.then(alert); // done! (shows up right now)
请注意,这使得 Promise 比实际的“订阅列表”场景更强大。如果歌手已经发布了他们的歌曲,然后有人在订阅列表上注册,他们可能收不到这首歌。现实生活中的订阅必须在事件发生之前完成。
Promise 更灵活。我们可以随时添加处理程序:如果结果已经存在,它们就会执行。
示例:loadScript
接下来,让我们看一些更实际的示例,了解 Promise 如何帮助我们编写异步代码。
我们有 loadScript
函数,用于从上一章加载脚本。
这是基于回调的变体,只是为了提醒我们
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
让我们使用 Promise 重写它。
新的函数 loadScript
不需要回调。相反,它将创建并返回一个 Promise 对象,该对象在加载完成后解析。外部代码可以使用 .then
向其添加处理程序(订阅函数)
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));
document.head.append(script);
});
}
用法
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('Another handler...'));
我们可以立即看到基于回调的模式的一些优势
Promise | 回调 |
---|---|
Promise 允许我们按自然顺序做事。首先,我们运行 loadScript(script) ,然后 .then 我们编写如何处理结果。 |
在调用 loadScript(script, callback) 时,我们必须准备好一个 callback 函数。换句话说,我们必须在调用 loadScript 之前 知道如何处理结果。 |
我们可以多次对 Promise 调用 .then 。每次调用,我们都向“订阅列表”中添加一个新的“粉丝”,即一个新的订阅函数。下一章将对此进行更详细的介绍:Promise 链式调用。 |
只能有一个回调函数。 |
因此,Promise 为我们提供了更好的代码流程和灵活性。但还有更多内容。我们将在下一章中看到。
评论
<code>
标签;要插入多行代码,请将其包装在<pre>
标签中;要插入 10 行以上的代码,请使用沙盒(plnkr、jsbin、codepen…)