--- title: 优雅的异步处理 slug: Learn/JavaScript/Asynchronous/Promises translation_of: Learn/JavaScript/Asynchronous/Promises original_slug: learn/JavaScript/异步/Promises语法 ---
Promise 是 JavaScript 语言的一个相对较新的功能,允许你推迟进一步的操作,直到上一个操作完成或响应其失败。这对于设置一系列异步操作以正常工作非常有用。本文向你展示了promises如何工作,如何在Web API中使用它们以及如何编写自己的API
前提条件: | 基本的计算机素养,具备基础的JavaScript知识 |
---|---|
目标: | 理解并使用学习如何使用Promises |
我们在教程的第一篇文章中简要地了解了 Promises,接下来我们将在更深层次理解Promise。
本质上,Promise 是一个对象,代表操作的中间状态 —— 正如它的单词含义 '承诺' ,它保证在未来可能返回某种结果。虽然 Promise 并不保证操作在何时完成并返回结果,但是它保证当结果可用时,你的代码能正确处理结果,当结果不可用时,你的代码同样会被执行,来优雅的处理错误。
通常你不会对一个异步操作从开始执行到返回结果所用的时间感兴趣(除非它耗时过长),你会更想在任何时候都能响应操作结果,当然它不会阻塞其余代码的执行就更好了。
你与 Promise 常见的交互之一就是 Web API 返回的 promise 对象。让我们设想一个视频聊天应用程序,该程序有一个展示用户的朋友列表的窗口,可以点击朋友旁边的按钮对朋友视频呼叫。
该按钮的处理程序调用 {{domxref("MediaDevices.getUserMedia", "getUserMedia()")}} 来访问用户的摄像头和麦克风。由于 getUserMedia()
必须确保用户具有使用这些设备的权限,并询问用户要使用哪个麦克风和摄像头(或者是否仅进行语音通话,以及其他可能的选项),因此它会产生阻塞,直到用户做出所有的决定,并且摄像头和麦克风都已启用。此外,用户可能不会立即响应权限请求。所以 getUserMedia()
可能需要很长时间。
由于 getUserMedia()
是在浏览器的主线程进行调用,整个浏览器将会处于阻塞状态直到 getUserMedia()
返回,这是不应该发生的;不使用Promise,浏览器将处于不可用状态直到用户为摄像头和麦克风做出决定。因此 getUserMedia()
返回一个Promise对象,即 {{jsxref("promise")}},一旦 {{domxref("MediaStream")}} 流可用才去解析,而不是等待用户操作、启动选中的设备并直接返回从所选资源创建的 {{domxref("MediaStream")}} 流。
上述视频聊天应用程序的代码可能像下面这样:
function handleCallButton(evt) { setStatusMessage("Calling..."); navigator.mediaDevices.getUserMedia({video: true, audio: true}) .then(chatStream => { selfViewElem.srcObject = chatStream; chatStream.getTracks().forEach(track => myPeerConnection.addTrack(track, chatStream)); setStatusMessage("Connected"); }).catch(err => { setStatusMessage("Failed to connect"); }); }
这个函数在开头调用 setStatusMessage()
来更新状态显示信息"Calling...", 表示正在尝试通话。接下来调用 getUserMedia()
,请求具有视频及音频轨的流,一旦获得这个流,就将其显示在"selfViewElem"的video元素中。接下来将这个流的每个轨道添加到表示与另一个用户的连接的 WebRTC,参见{{domxref("RTCPeerConnection")}}。在这之后,状态显示为"Connected"。
如果getUserMedia()
失败,则catch块运行。这使用setStatusMessage()
更新状态框以指示发生错误。
这里重要的是getUserMedia()
调用几乎立即返回,即使尚未获得相机流。即使handleCallButton()
函数向调用它的代码返回结果,当getUserMedia()
完成工作时,它也会调用你提供的处理程序。只要应用程序不假设流式传输已经开始,它就可以继续运行。
注意: 如果你有兴趣,可以在文章Signaling and video calling中了解有关此高级主题的更多信息。在该示例中使用与此类似的代码,但更完整。
要完全理解为什么 promise 是一件好事,应该回想之前的回调函数,理解它们造成的困难。
我们来谈谈订购披萨作为类比。为了使你的订单成功,你必须按顺序执行,不按顺序执行或上一步没完成就执行下一步是不会成功的:
对于旧式callbacks,上述功能的伪代码表示可能如下所示:
chooseToppings(function(toppings) { placeOrder(toppings, function(order) { collectOrder(order, function(pizza) { eatPizza(pizza); }, failureCallback); }, failureCallback); }, failureCallback);
这很麻烦且难以阅读(通常称为“回调地狱”),需要多次调用failureCallback()
(每个嵌套函数一次),还有其他问题。
Promises使得上面的情况更容易编写,解析和运行。如果我们使用异步promises代表上面的伪代码,我们最终会得到这样的结果:
chooseToppings() .then(function(toppings) { return placeOrder(toppings); }) .then(function(order) { return collectOrder(order); }) .then(function(pizza) { eatPizza(pizza); }) .catch(failureCallback);
这要好得多 - 更容易看到发生了什么,我们只需要一个.catch()
块来处理所有错误,它不会阻塞主线程(所以我们可以在等待时继续玩视频游戏为了准备好收集披萨),并保证每个操作在运行之前等待先前的操作完成。我们能够以这种方式一个接一个地链接多个异步操作,因为每个.then()
块返回一个新的promise,当.then()
块运行完毕时它会解析。聪明,对吗?
使用箭头函数,你可以进一步简化代码:
chooseToppings() .then(toppings => placeOrder(toppings) ) .then(order => collectOrder(order) ) .then(pizza => eatPizza(pizza) ) .catch(failureCallback);
甚至这样:
chooseToppings() .then(toppings => placeOrder(toppings)) .then(order => collectOrder(order)) .then(pizza => eatPizza(pizza)) .catch(failureCallback);
这是有效的,因为使用箭头函数 () => x
是 ()=> {return x;}
的有效简写; 。
你甚至可以这样做,因为函数只是直接传递它们的参数,所以不需要额外的函数层:
chooseToppings().then(placeOrder).then(collectOrder).then(eatPizza).catch(failureCallback);
但是,这并不容易阅读,如果你的块比我们在此处显示的更复杂,则此语法可能无法使用。
注意: 你可以使用 async/await
语法进行进一步的改进,我们将在下一篇文章中深入讨论。
最基本的,promise与事件监听器类似,但有一些差异:
Promises 很重要,因为大多数现代Web API都将它们用于执行潜在冗长任务的函数。要使用现代Web技术,你需要使用promises。在本章的后面我们将看看如何编写自己的promises,但是现在我们将看一些你将在Web API中遇到的简单示例。
在第一个示例中,我们将使用fetch()
方法从Web获取图像,blob()
方法来转换获取响应的原始内容到 Blob 对象,然后在 <img> 元素内显示该blob
。这与我们在 first article of the series中看到的示例非常相似,但是会在构建你自己的基于 promise 的代码时有所不同。
注意: 下列代码无法直接运行(i.e. via a file://
URL)。你需要本地测试服务器,或是 Glitch 和 GitHub pages 这样的在线解决方案。
首先,下载我们的 simple HTML template和sample image file。
将 {{htmlelement("script")}} 元素添加在HTML {{htmlelement("body")}} 的底部。
在 {{HTMLElement("script")}} 元素内,添加以下行:
let promise = fetch('coffee.jpg');
这会调用 fetch()
方法,将图像的URL作为参数从网络中提取。这也可以将options对象作为可选的第二个参数,但我们现在只使用最简单的版本。我们将 fetch()
返回的promise对象存储在一个名为promise的变量中。正如我们之前所说的,这个对象代表了一个最初既不成功也不失败的中间状态 - 这个状态下的promise的官方术语叫作pending。
then()
方法。 .then()
块中的回调(称为执行程序)仅在promise调用成功完成时运行并返回{{domxref("Response")}}对象 - 在promise-speak中,当它已被满足时。它将返回的{{domxref("Response")}}对象作为参数传递。注意: .then()
块的工作方式类似于使用AddEventListener()
向对象添加事件侦听器时的方式。它不会在事件发生之前运行(当promise履行时)。最显着的区别是.then()
每次使用时只运行一次,而事件监听器可以多次调用。
我们立即对此响应运行blob()
方法以确保响应主体完全下载,并且当它可用时将其转换为我们可以执行某些操作的Blob
对象。返回的结果如下:
response => response.blob()
这是下面的简写
function(response) { return response.blob(); }
好的,我们还需要做点额外的工作。Fetch promises 不会产生 404 或 500错误,只有在产生像网路故障的情况时才会不工作。总的来说,Fetch promises 总是成功运行,即使response.ok 属性是 false
。为了产生404错误,我们需要判断 response.ok
,如果是 false
,抛出错误,否则返回 blob。就像下面的代码这样做。
let promise2 = promise.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} else {
return response.blob();
}
});
5. 每次调用.then()
都会创建一个新的promise。这非常有用;因为blob()
方法也返回一个promise,我们可以通过调用第二个promise的.then()
方法来处理它在履行时返回的Blob
对象。因为我们想要对blob
执行一些更复杂的操作,而不仅仅运行单个方法并返回结果,这次我们需要将函数体包装成花括号(否则会抛出错误)。
将以下内容添加到代码的末尾:
let promise3 = promise2.then(myBlob => {})
6. 现在让我们填写执行程序函数的主体。在花括号内添加以下行:
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
这里我们运行{{domxref("URL.createObjectURL()")}}方法,将其作为Blob
在第二个promise实现时返回的参数传递。这将返回指向该对象的URL。然后我们创建一个{{htmlelement("img")}}元素,将其src
属性设置为等于对象URL并将其附加到DOM,这样图像就会显示在页面上!
如果你保存刚刚创建的HTML文件并将其加载到浏览器中,你将看到图像按预期显示在页面中。干得好!
注意: 你可能会注意到这些例子有点做作。你可以取消整个fetch()
和blob()
链,只需创建一个<img>元素并将其src
属性值设置为图像文件的URL,即coffee.jpg
。然而,我们选择了这个例子,因为它以简单的方式展示了promise,而不是真实世界的适当性。
缺少一些东西 - 如果其中一个promise失败(rejects,in promise-speak),目前没有什么可以明确地处理错误。我们可以通过运行前一个promise的 .catch()
方法来添加错误处理。立即添加:
let errorCase = promise3.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
要查看此操作,请尝试拼错图像的URL并重新加载页面。该错误将在浏览器的开发人员工具的控制台中报告。
如果你根本不操心包括的 .catch()
块,这并没有做太多的事情,但考虑一下(指.catch()块) ––这会使我们可以完全控制错误处理方式。在真实的应用程序中,你的.catch()
块可以重试获取图像,或显示默认图像,或提示用户提供不同的图像URL等等。
注意: 你可以参考 our version of the example live (参阅 source code ).
这是写出来的一种非常简便的方式;我们故意这样做是为了帮助你清楚地了解发生了什么。如本文前面所示,你可以将.then()
块(以及.catch()
块)链接在一起。上面的代码也可以这样写(参阅GitHub上的simple-fetch-chained.html ):
fetch('coffee.jpg')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} else {
return response.blob();
}
})
.then(myBlob => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
请记住,履行的promise所返回的值将成为传递给下一个 .then()
块的executor函数的参数。
注意: promise中的.then()/catch()
块基本上是同步代码中try...catch
块的异步等价物。请记住,同步try ... catch
在异步代码中不起作用。
在上面的部分中有很多要介绍的内容,所以让我们快速回过头来给你一个简短的指南,你可以将它添加到书签中,以便将来更新你的记忆。你还应该再次阅读上述部分,以确保这些概念坚持下去。
.then()
块链接到promise链的末尾来访问该值。 .then()
块中的执行程序函数将包含promise的返回值。.catch()
块链接到promise链的末尾来访问此原因。上面的例子向我们展示了使用promises的一些真正的基础知识。现在让我们看一些更高级的功能。首先,链接进程一个接一个地发生都很好,但是如果你想在一大堆Promises全部完成之后运行一些代码呢?
你可以使用巧妙命名的Promise.all()静态方法完成此操作。这将一个promises数组作为输入参数,并返回一个新的Promise对象,只有当数组中的所有promise都满足时才会满足。它看起来像这样:
Promise.all([a, b, c]).then(values => { ... });
如果它们都实现,那么数组中的结果将作为参数传递给.then()
块中的执行器函数。如果传递给Promise.all()
的任何一个 promise 拒绝,整个块将拒绝。
这非常有用。想象一下,我们正在获取信息以在内容上动态填充页面上的UI功能。在许多情况下,接收所有数据然后才显示完整内容,而不是显示部分信息。
让我们构建另一个示例来展示这一点。
下载我们页面模板(page template)的新副本,并再次在结束</ body>标记之前放置一个<script>元素。
下载我们的源文件(coffee.jpg, tea.jpg和 description.txt),或者随意替换成你自己的文件。
在我们的脚本中,我们将首先定义一个函数,该函数返回我们要发送给Promise.all()
的promise。如果我们只想运行Promise.all()
块以响应三个fetch()
操作完成,这将很容易。我们可以这样做:
let a = fetch(url1); let b = fetch(url2); let c = fetch(url3); Promise.all([a, b, c]).then(values => { ... });
当promise是fullfilled时,传递到履行处理程序的values
将包含三个Response对象,每个对象用于已完成的每个fetch()
操作。
但是,我们不想这样做。我们的代码不关心fetch()
操作何时完成。相反,我们想要的是加载的数据。这意味着当我们返回代表图像的可用blob
和可用的文本字符串时,我们想要运行Promise.all()
块。我们可以编写一个执行此操作的函数;在<script>
元素中添加以下内容:
function fetchAndDecode(url, type) { return fetch(url).then(response => { if (type === 'blob') { return response.blob(); } else if (type === 'text') { return response.text(); } }) .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); }
这看起来有点复杂,所以让我们一步一步地完成它:
fetch()
函数来获取指定URL处的资源,然后将其链接到另一个 promise ,它解码(或“read”)响应body。这是前一个示例中的blob()
方法。if ... else if
语句,根据我们需要解码的文件类型返回不同的promise(在这种情况下,我们可以选择blob
或text
,而且很容易扩展这个以处理其他类型)。fetch()
调用之前添加了return
关键字。它的作用是运行整个链,然后运行最终结果(即blob()
或text()
返回的promise作为我们刚刚定义的函数的返回值)。实际上,return
语句将结果从链返回到顶部。在块结束时,我们链接一个.catch()
调用,以处理任何可能出现在数组中传递给.all()
的任何promise的错误情况。如果任何promise被拒绝,catch
块将告诉你哪个promise有问题。 .all()
块(见下文)仍然可以实现,但不会显示有问题的资源。如果你想要.all
拒绝,你必须将.catch()
块链接到那里的末尾。
函数体内部的代码是async(异步)和基于promise的,因此实际上整个函数就像一个promise ––方便啊!
接下来,我们调用我们的函数三次以开始获取和解码图像和文本的过程,并将每个返回的promises存储在变量中。在以前的代码下面添加以下内容:
let coffee = fetchAndDecode('coffee.jpg', 'blob'); let tea = fetchAndDecode('tea.jpg', 'blob'); let description = fetchAndDecode('description.txt', 'text');
接下来,我们将定义一个Promise.all()
块,仅当上面存储的所有三个promise都已成功完成时才运行一些代码。首先,在.then()
调用中添加一个带有空执行程序的块,如下所示:
Promise.all([coffee, tea, description]).then(values => { });
你可以看到它需要一个包含promises作为参数的数组。执行者只有在所有三个promises的状态成为resolved时才会运行;当发生这种情况时,它将被传入一个数组,其中包含来自各个promise(即解码的响应主体)的结果,类似于 [coffee-results, tea-results, description-results].
最后,在执行程序中添加以下内容。这里我们使用一些相当简单的同步代码将结果存储在单独的变量中(从blob创建对象URL),然后在页面上显示图像和文本。
console.log(values); // Store each value returned from the promises in separate variables; create object URLs from the blobs let objectURL1 = URL.createObjectURL(values[0]); let objectURL2 = URL.createObjectURL(values[1]); let descText = values[2]; // Display the images in <img> elements let image1 = document.createElement('img'); let image2 = document.createElement('img'); image1.src = objectURL1; image2.src = objectURL2; document.body.appendChild(image1); document.body.appendChild(image2); // Display the text in a paragraph let para = document.createElement('p'); para.textContent = descText; document.body.appendChild(para);
保存并刷新,你应该看到所有UI组件都已加载,尽管不是特别有吸引力!
我们在这里提供的用于显示项目的代码相当简陋,但现在作为解释器。
注意: 如果你遇到困难,可以将你的代码版本与我们的代码进行比较,看看它的外观 -––see it live,也可以参阅source code。
注意: 如果你正在改进这段代码,你可能想要遍历一个项目列表来显示,获取和解码每个项目,然后循环遍历Promise.all()
内部的结果,运行一个不同的函数来显示每个项目取决于什么代码的类型是。这将使它适用于任何数量的项目,而不仅仅是三个。
此外,你可以确定要获取的文件类型,而无需显式类型属性。例如,你可以使用response.headers.get("content-type")
检查响应的{{HTTPHeader("Content-Type")}} HTTP标头,然后做出相应的反应。
在promise完成后,你可能希望运行最后一段代码,无论它是否已实现(fullfilled)或被拒绝(rejected)。此前,你必须在.then()
和.catch()
回调中包含相同的代码,例如:
myPromise .then(response => { doSomething(response); runFinalCode(); }) .catch(e => { returnError(e); runFinalCode(); });
在现代浏览器中,.finally()
方法可用,它可以链接到常规promise链的末尾,允许你减少代码重复并更优雅地执行操作。上面的代码现在可以写成如下:
myPromise .then(response => { doSomething(response); }) .catch(e => { returnError(e); }) .finally(() => { runFinalCode(); });
有关一个真实示例,请查看我们的promise-finally.html demo(另请参阅source code)。这与我们在上面部分中看到的Promise.all()
演示完全相同,除了在fetchAndDecode()
函数中我们将finally()
调用链接到链的末尾:
function fetchAndDecode(url, type) {
return fetch(url).then(response => {
if(!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} else {
if(type === 'blob') {
return response.blob();
} else if(type === 'text') {
return response.text();
}
}
})
.catch(e => {
console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message);
})
.finally(() => {
console.log(`fetch attempt for "${url}" finished.`);
});
}
这会将一条简单的消息记录到控制台,告诉我们每次获取尝试的时间。
注意:finally()允许你在异步代码中编写异步等价物try/ catch / finally。
好消息是,在某种程度上,你已经建立了自己的promise。当你使用.then()
块链接多个promise时,或者将它们组合起来创建自定义函数时,你已经在创建自己的基于异步声明的自定义函数。例如,从前面的示例中获取我们的fetchAndDecode()
函数。
将不同的基于promise的API组合在一起以创建自定义函数是迄今为止你使用promises进行自定义事务的最常见方式,并展示了基于相同原则的大多数现代API的灵活性和强大功能。然而,还有另一种方式。
可以使用Promise()
构造函数构建自己的promise。当你需要使用现有的旧项目代码、库或框架以及基于现代promise的代码时,这会派上用场。比如,当你遇到没有使用promise的旧式异步API的代码时,你可以用promise来重构这段异步代码。
让我们看一个简单的示例来帮助你入门 —— 这里我们用 promise 包装一了个setTimeout(),
它会在两秒后运行一个函数,该函数将用字符串“Success!”,解析当前promise(调用链接的resolve()
)。
let timeoutPromise = new Promise((resolve, reject) => { setTimeout(function(){ resolve('Success!'); }, 2000); });
resolve()
和reject()
是用来实现和拒绝新创建的promise的函数。此处,promise 成功运行通过显示字符串“Success!”。
因此,当你调用此promise时,可以将.then()
块链接到它的末尾,它将传递给.then()
块一串“Success!”。在下面的代码中,我们显示出该消息:
timeoutPromise .then((message) => { alert(message); })
更简化的版本:
timeoutPromise.then(alert);
尝试 running this live 以查看结果 (可参考 source code).
上面的例子不是很灵活 - promise只能实现一个字符串,并且它没有指定任何类型的reject()
条件(诚然,setTimeout()
实际上没有失败条件,所以对这个简单的例子并不重要)。
注意: 为什么要resolve()
,而不是fullfill()
?我们现在给你的答案有些复杂。
我们可以创建一个reject()
方法拒绝promise - 就像resolve()
一样,这需要一个值,但在这种情况下,它是拒绝的原因,即将传递给.catch()
的错误块。
让我们扩展前面的例子,使其具有一些reject()
条件,并允许在成功时传递不同的消息。
获取previous example副本,并将现有的timeoutPromise()
定义替换为:
function timeoutPromise(message, interval) { return new Promise((resolve, reject) => { if (message === '' || typeof message !== 'string') { reject('Message is empty or not a string'); } else if (interval < 0 || typeof interval !== 'number') { reject('Interval is negative or not a number'); } else { setTimeout(function(){ resolve(message); }, interval); } }); };
在这里,我们将两个方法传递给一个自定义函数 - 一个用来做某事的消息,以及在做这件事之前要经过的时间间隔。在函数内部,我们返回一个新的Promise
对象 - 调用该函数将返回我们想要使用的promise。
在Promise
构造函数中,我们在if ... else结构中进行了一些检查:
setTimeout()
在指定的时间间隔过后,使用指定的消息解析promise。由于timeoutPromise()
函数返回一个Promise
,我们可以将.then()
,.catch()
等链接到它上面以利用它的功能。现在让我们使用它 - 将以前的timeoutPromise用法替换为以下值:
timeoutPromise('Hello there!', 1000) .then(message => { alert(message); }) .catch(e => { console.log('Error: ' + e); });
当你按原样保存并运行代码时,一秒钟后你将收到消息提醒。现在尝试将消息设置为空字符串或将间隔设置为负数,例如,你将能够通过相应的错误消息查看被拒绝的promise!你还可以尝试使用已解决的消息执行其他操作,而不仅仅是提醒它。
注意: 你可以在GitHub上找到我们的这个示例版本custom-promise2.html(另请参阅source code)。
上面的例子是故意做得简单,以使概念易于理解,但它并不是实际上完全异步。异步性质基本上是使用setTimeout()
伪造的,尽管它仍然表明promises对于创建具有合理的操作流程,良好的错误处理等的自定义函数很有用
我们想邀请你学习的一个例子是Jake Archibald's idb library,它真正地显示了Promise()
构造函数的有用异步应用程序。这采用了 IndexedDB API,它是一种旧式的基于回调的API,用于在客户端存储和检索数据,并允许你将其与promises一起使用。如果你查看main library file,你将看到我们在上面讨论过的相同类型的技术。以下块将许多IndexedDB方法使用的基本请求模型转换为使用promise:
function promisifyRequest(request) { return new Promise(function(resolve, reject) { request.onsuccess = function() { resolve(request.result); }; request.onerror = function() { reject(request.error); }; }); }
这可以通过添加一些事件处理程序来实现,这些事件处理程序在适当的时候实现(fullfill)和拒绝(reject)promise。
request
的success
event触发时,onsuccess
处理程序将使用请求的result
实现(fullfill)promise。request
的error
event触发时,onerror
处理程序拒绝带有请求error
的promise当我们不知道函数的返回值或返回需要多长时间时,Promises是构建异步应用程序的好方法。它们使得在没有深度嵌套回调的情况下更容易表达和推理异步操作序列,并且它们支持类似于同步try ... catch
语句的错误处理方式。
Promise适用于所有现代浏览器的最新版本;promise有兼容问题的唯一情况是Opera Mini和IE11及更早版本。
本文中,我们没有涉及的所有promise的功能,只是最有趣和最有用的功能。当你开始了解有关promise的更多信息时,你会遇到更多功能和技巧。
大多数现代Web API都是基于promise的,因此你需要了解promise才能充分利用它们。这些API包括WebRTC,Web Audio API,Media Capture and Streams等等。随着时间的推移,Promises将变得越来越重要,因此学习使用和理解它们是学习现代JavaScript的重要一步。
参见
Promise()
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Timeouts_and_intervals", "Learn/JavaScript/Asynchronous/Async_await", "Learn/JavaScript/Asynchronous")}}