重学JavaScript Promise API
在这篇教程中,我们将掌握如何在JavaScript中创建并使用Promise。我们将了解Promise链式调用、错误处理以及最近添加到语言中的一些Promise静态方法。
什么是Promise?
在JavaScript中,一些操作是异步的。这意味着当这些操作完成时,它们产出的结果或者值并不会立即生效。
Promise是一个特殊的JavaScript对象,它代表了异步操作的最终结果。它就像操作结果的代理。
回调函数
在拥有JavaScript Promise之前,处理异步操作最优雅的方式是使用回调。当异步操作的结果就绪时,回调就是一个运行的函数。比如说:
setTimeout(function() {
console.log('Hello, World!');
}, 1000);
这里,setTimeout是一个异步函数,在指定的毫秒数后运行传递给它的回调函数。在本例中,它在一秒后将 "Hello, World!"打印到控制台。
此时想象我们想要在五秒之内每秒都打印一个信息。代码就会是这样的:
setTimeout(function() {
console.log(1);
setTimeout(function() {
console.log(2);
setTimeout(function() {
console.log(3);
setTimeout(function() {
console.log(4);
setTimeout(function() {
console.log(5);
}, 1000);
}, 1000);
}, 1000);
}, 1000);
}, 1000);
以这种方式使用多个嵌套回调的异步JavaScript既容易出错又难以维护。它通常被称为回调地狱,甚至有自己的网页。
当然,这是一个臆造的例子,但它有助于说明问题。在实际场景中,我们可能会进行Ajax调用,用结果更新DOM,然后等待动画完成。或者,我们的服务器可能从客户端接收输入,验证输入,更新数据库,写入日志文件,最后发送响应。在这两种情况下,我们还需要处理发生的任何错误。
使用嵌套回调来完成这样的任务是非常痛苦的。幸运的是,Promise为我们提供了一种更简洁的语法,使我们能够将异步命令串联起来,让它们一个接一个地运行。
创建Promise
创建Promise的基本语法如下:
const promise = new Promise((resolve, reject) => {
//asynchronous code goes here
});
首先,我们使用Promise
构造函数实例化一个新的Promise对象,并传递给它一个回调函数。回调接收两个参数:resolve
和reject
,它们都是函数。我们所有的异步代码都在回调函数中。
如果一切运行成功,则通过调用 resolve
来实现Promise。如果出现错误,则调用 reject
拒绝Promise。我们可以向这两个方法传递值,这些值将在消费代码中可用。
要了解这在实践中是如何工作的,请参考下面的代码。该代码向web服务发出异步请求,以 JSON 格式返回一个随机的笑话:
const promise = new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.open('GET', '<https://icanhazdadjoke.com/>');
request.setRequestHeader('Accept', 'application/json');
request.onload = () => {
if (request.status === 200) {
resolve(request.response); // we got data here, so resolve the Promise
} else {
reject(Error(request.statusText)); // status is not 200 OK, so reject
}
};
request.onerror = () => {
reject(Error('Error fetching data.')); // error occurred, reject the Promise
};
request.send(); // send the request
});
Promise构造函数
我们首先使用Promise
构造函数创建一个新的Promise对象。该构造函数用于封装尚未支持Promise的函数或API,例如上面的XMLHttpRequest
对象。传递给Promise构造函数的回调包含用于从远程服务获取数据的异步代码。(注意,我们在这里使用的是箭头函数)在回调中,我们向 https://icanhazdadjoke.com/ 创建了一个 Ajax 请求,该请求以 JSON 格式返回一个随机的笑话。
当从远程服务器收到成功的响应时,会传递给resolve
方法。如果发生任何错误(无论是在服务器上还是在网络层),reject
方法将调用一个Error
对象。
then方法
当我们实例化一个Promise对象时,我们将得到一个未来可用数据的代理。在我们的例子中,我们期待从远程服务返回一些数据。那么,我们如何知道数据何时可用呢?这就是使用Promise.then()
函数的地方:
const promise = new Promise((resolve, reject) => { ... });
promise.then((data) => {
console.log('Got data! Promise fulfilled.');
document.body.textContent = JSON.parse(data).joke;
}, (error) => {
console.error('Promise rejected.');
console.error(error.message);
});
该函数可以接受两个参数:成功回调和失败回调。这些回调将在Promise解决(即fulfilled
或rejected
)时调用。如果Promise实现,成功回调将使用我们传递给resolve
的实际数据触发。如果Promise被拒绝,失败回调将被调用。无论我们传递给reject
的是什么,都将作为参数传递给该回调。
Promise的状态
在上面代码中,我们可以通过调用resolve
和reject
方法来改变Promise的状态。在继续之前,花点时间看下Promise的生命周期。
Promise的状态会是下面值的其中一种:
- pending
- fulfilled
- rejected
- settled
Promise开始时处于pending
的状态。这意味着它既没有fulfilled
也没有rejected
。如果与Promise相关的操作成功(在我们的示例中是远程 API 调用),并且调用了 resolve
方法,那么Promise称为fulfilled
。另一方面,如果相关操作不成功,且 reject
方法被调用,则该Promise处于rejected
状态。最后,如果Promise处于fulfilled
或rejected
状态,但不是pending
状态,则称为settled
。
一旦Promise是rejected
或者fulfilled
,该状态将永久与之关联。这意味着Promise只能成功或失败一次。如果Promise已经fulfilled
,并且在其后附加有两个回调的then()
,那么成功回调会直接被调用。因此,在Promise的世界里,我们不关心Promise何时settled
。我们只关心Promise的最终结果。
Promise链式调用
有时可能需要将多个异步任务按照特定顺序链在一起。这就是所谓的Promise链式调用。让我们重温一下 setTimeout
示例,以了解Promise链式调用的基本工作原理。
我们可以像以前一样,首先创建一个新的Promise对象:
const promise = new Promise((resolve, reject) => {
setTimeout(() => { resolve() }, 1000)
});
promise.then(() => {
console.log(1);
});
不出所料,Promise在一秒后被执行,控制台打印"1"。
为了继续链式调用,我们需要在控制台语句后返回第二个Promise,并将其传递给第二个then
:
const promise = new Promise((resolve, reject) => {
setTimeout(() => { resolve() }, 1000)
});
promise.then(() => {
console.log(1);
return new Promise((resolve, reject) => {
setTimeout(() => { resolve() }, 1000)
});
}).then(() => {
console.log(2);
});
虽然这个方法可行,但它已经开始变得有点笨重。让我们创建一个返回新Promise的函数,并在特定时间后解析该Promise:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
然后,我们可以使用它来扁平化嵌套代码:
sleep(1000)
.then(() => {
console.log(1);
return sleep(1000);
}).then(() => {
console.log(2);
return sleep(1000);
}).then(() => {
console.log(3);
return sleep(1000);
})
...
由于 then
方法本身返回一个 Promise 对象,并且我们不会从一个异步操作传递任何值到下一个异步操作,这使得我们能够进一步简化事情:
sleep(1000)
.then(() => console.log(1))
.then(() => sleep(1000))
.then(() => console.log(2))
.then(() => sleep(1000))
.then(() => console.log(3))
...
这比原始代码要优雅得多。
请注意,如果你想了解更多有关使用JavaScript实现一个sleep函数,你可能对这篇文章感兴趣。
向下传递数据
当我们需要执行多个异步操作时,我们可能希望将一个异步调用的结果传递给Promise链中的下一个then
,这样我们就可以对该数据进行处理。
例如,我们可能想要获取 GitHub 仓库的贡献者列表,然后使用该信息获取第一位贡献者的姓名:
fetch('<https://api.github.com/repos/eslint/eslint/contributors>')
.then(res => res.json())
.then(json => {
const firstContributor = json[0].login;
return fetch(`https://api.github.com/users/${firstContributor}`)
})
.then(res => res.json())
.then(json => console.log(`The first contributor to ESLint was ${json.name}`));
// The first contributor to ESLint was Nicholas C. Zakas
正如我们看到的,通过返回从第二个 fetch
调用返回的Promise,服务器的响应 (res
) 在下面的 then
中可用。
Promise错误处理
我们已经知道,then函数接收两个回调函数作为参数,并且如果Promise被拒绝,第二个参数会被调用:
promise.then((data) => {
console.log('Got data! Promise fulfilled.');
...
}, (error) => {
console.error('Promise rejected.');
console.error(error.message);
});
然而,为每个Promise指定错误处理函数是相当繁琐的,尤其是处理Promise链式调用的时候。幸运的是,还有更好的方式。
catch方法
我们还可以使用catch
方法,它可以为我们处理错误。当一个Promise在Promise链的任何地方rejected
时,控制会跳转到最近的拒绝处理函数中。这非常方便,因为它意味着我们可以在链的末尾添加一个catch
,让它来处理发生的任何错误。
让我们以前面的代码为例:
fetch('<https://api.github.com/repos/eslint/eslint/contributors>')
.then(res => res.json())
.then(json => {
const firstContributor = json[0].login;
return fetch(`https://api.github.com/users/${firstContributor}`)
})
.then(res => res.jsn())
.then(json => console.log(`The top contributor to ESLint wass ${json.name}`))
.catch(error => console.log(error));
注意,除了在代码块的末尾添加错误处理函数之外,我还在第7行将res.json()
拼错为res.jsn()
。
现在运行代码,会在屏幕上看到下面的输出:
TypeError: res.jsn is not a function
<anonymous> <http://0.0.0.0:8000/index.js:7>
promise callback* <http://0.0.0.0:8000/index.js:7>
index.js:9:27
我正在运行的文件名为index.js
。第7行包含错误,第9行是捕获错误的catch
块。
finally方法
Promise.finally
方法在Promise settled
后运行,也就是resolved
或者rejected
。与catch
一样,该方法有助于防止代码重复,并且在执行清理任务时非常有用,例如关闭数据库连接或从UI中移除加载动画。
下面是一个使用我们之前代码的示例:
function getFirstContributor(org, repo) {
showLoadingSpinner();
fetch(`https://api.github.com/repos/${org}/${repo}/contributors`)
.then(res => res.json())
.then(json => {
const firstContributor = json[0].login;
return fetch(`https://api.github.com/users/${firstContributor}`)
})
.then(res => res.json())
.then(json => console.log(`The first contributor to ${repo} was ${json.name}`))
.catch(error => console.log(error))
.finally(() => hideLoadingSpinner());
};
getFirstContributor('facebook', 'react');
它不接收任何参数并返回一个Promise,因此我们可以在它的返回值上链式调用更多的then
、catch
和finally
调用。
更多Promise方法
到此为止,我们已经对JavaScript Promise有了一个很好的基本了解,但在结束之前,我们需要注意各种Promise实用方法。
Promise.all()
在前面的示例中,我们需要在第一个 Ajax 调用完成后才能进行第二个 Ajax 调用。与此不同的是,有时我们会有一堆完全不相互依赖的异步操作。这时就需要使用 Promise.all
。
该方法接收一个Promise数组,等待所有Promise resolved
或其中任何一个Promise rejected
。如果所有的Promise都成功resolved
,all
实现一个数组,该数组包含各个Promise的履行值:
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 0)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
.then(values => console.log(values))
.catch(err => console.error(err));
上述代码会在三秒后在控制台打印[1, 2, 3]
。
然而,如果任何Promise rejected
,all
将拒绝该Promise的值,而不会考虑任何其他Promise。
Promise.allSettled()
不像all
,Promise.allSettled
将等待传递给它的每一个Promise的实现或拒绝。如果一个Promise被拒绝,它不会停止执行:
Promise.allSettled([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 0)),
new Promise((resolve, reject) => setTimeout(() => reject(2), 1500)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
.then(values => console.log(values))
.catch(err => console.error(err));
这将返回状态和值(如果fulfilled
)或者原因(如果rejected
)的列表:
[
{ status: "fulfilled", value: 1 },
{ status: "rejected", reason: 2 },
{ status: "fulfilled", value: 3 },
]
Promise.any()
Promise.any()
返回第一个状态为fulfilled
的Promise的值。如果有任何Promise rejected
,都会被忽略:
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(1), 0)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
.then(values => console.log(values))
.catch(err => console.error(err));
在1.5秒后,控制台会打印"2"。
Promise.race()
Promise.race
也接收一个Promise数组,并(像上面列出的其他方法一样)返回一个新的Promise。只要它接收到的一个Promise实现或者拒绝,race
本身就会使用刚刚settled
的Promise的值或原因来实现或拒绝:
Promise.race([
new Promise((resolve, reject) => setTimeout(() => reject('Rejected with 1'), 0)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
.then(values => console.log(values))
.catch(err => console.error(err));
这会在控制台打印”Rejected with 1”,因为数组中的第一个Promise会被立即拒绝,并且拒绝会被我们的catch
块捕获。
我们可以这么改:
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve('Resolved with 1'), 0)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
.then(values => console.log(values))
.catch(err => console.error(err));
这会在控制台打印”Resolved with 1”。
这两个例子中,其他两个Promise都会被忽略。
应该使用哪个
到目前为止,我们已经了解了回调和Promise,但值得一提的还有较新的async ... await
语法。虽然它实际上只是Promise之上的语法糖,但在很多情况下,它可以让基于Promise的代码更容易阅读和理解。
例如,我们可以这样重写之前的代码:
async function getFirstContributor(org, repo) {
showLoadingSpinner();
try {
const res1 = await fetch(`https://apiy.github.com/repos/${org}/${repo}/contributors`);
const contributors = await res1.json();
const firstContributor = contributors[0].login;
const res2 = await fetch(`https://api.github.com/users/${firstContributor}`)
const details = await res2.json();
console.log(`The first contributor to ${repo} was ${details.name}`);
} catch (error) {
console.error(error)
} finally {
hideLoadingSpinner();
}
}
getFirstContributor('facebook', 'react');
正如我们所看到的,我们使用try ... catch
语法来处理错误,并且我们可以在finally
块中进行任何修整。
我发现上述代码比基于Promise的版本更容易解析。不过,我鼓励你熟悉async ... await
语法,看看哪种最适合你。
总结
在本文中,我们了解了如何创建和使用 JavaScript Promise。我们学习了如何创建一个Promise链,并将数据从一个异步操作传递到下一个异步操作。我们还研究了错误处理以及各种Promise实用方法。
如上所述,下一步应该是开始学习async ...await
,加深对JavaScript程序内部流程控制的理解。
以上就是本文的全部内容。如果对你有所帮助,欢迎点赞、收藏、转发~
重学JavaScript Promise API的更多相关文章
- 【译】JavaScript Promise API
原文地址:JavaScript Promise API 在 JavaScript 中,同步的代码更容易书写和 debug,但是有时候出于性能考虑,我们会写一些异步的代码(代替同步代码).思考这样一个场 ...
- 重学前端 --- Promise里的代码为什么比setTimeout先执行?
首先通过一段代码进入讨论的主题 var r = new Promise(function(resolve, reject){ console.log("a"); resolve() ...
- JavaScript Promise API
同步编程通常来说易于调试和维护,然而,异步编程通常能获得更好的性能和更大的灵活性.异步的最大特点是无需等待."Promises"渐渐成为JavaScript里最重要的一部分,大量的 ...
- 重学JavaScript - 数组
作者:狐狸家的鱼 GitHub:surRimn 整理自MDN文档 数组 数组是一种类列表对象,长度和元素类型不固定. 描述 访问数组 JavaScript数组的索引是从0开始的,第一个元素的索引为0, ...
- 2.重学javascript 对象和数组
什么是对象,其实就是一种类型,即引用类型. 一.创建Object类型有两种. ①使用new运算符 <script type="text/javascript"> var ...
- 1.重学javascript (一)
一.script标签解析 <script>xxx</script>这组标签,是用于在html 页面中插入js的主要方法.它主要有以下 几个属性: 1.charset:可选.表示 ...
- 重学JavaScript - 映射与集合
作者:狐狸家的鱼 GitHub:surRimn 整理自MDN文档 带键的集合 映射 Map对象 一个Map对象在迭代时会根据对象中元素的插入顺序来进行 — 一个 for...of 循环在每次迭代后会返 ...
- 复习 Array,重学 JavaScript
1 数组与对象 在 JavaScript 中,一个对象的键只能有两种类型:string 和 symbol.下文只考虑键为字符串的情况. 1.1 创建对象 在创建对象时,若对象的键为数字,或者由 字母+ ...
- 重学JavaScript之面向对象的程序设计(继承)
1. 继承 ES 中只支持实现继承,而且其实现继承主要依靠原型链来实现的. 2. 原型链 ES中 描述了 原型链的概念,并将原型链作为实现继承的主要方法.其基本思想是利用原型让一个引用类型继承另一个引 ...
- 重学JavaScript之匿名函数
1. 什么是匿名函数? 匿名函数就是没有名字的函数,有时候也称为< 拉姆达函数>.匿名函数是一种强大的令人难以置信的工具.如下: function a(a1, a2, a3) { // 函 ...
随机推荐
- ersync 实时同步
ersync 实时同步 目录 ersync 实时同步 实时同步概述 结合sersync+rsync实时同步实战 环境准备 部署sersync(客户端) 实时同步概述 什么是实时同步 实时同步是一种只要 ...
- 音视频八股文(6)-- ffmpeg大体介绍和内存模型
播放器框架 常用音视频术语 • 容器/文件(Conainer/File):即特定格式的多媒体文件, 比如mp4.flv.mkv等. • 媒体流(Stream):表示时间轴上的一段连续数据,如一 段声音 ...
- 2023-01-06:给定一个只由小写字母组成的字符串str,长度为N, 给定一个只由0、1组成的数组arr,长度为N, arr[i] == 0表示str中i位置的字符不许修改, arr[i] ==
2023-01-06:给定一个只由小写字母组成的字符串str,长度为N, 给定一个只由0.1组成的数组arr,长度为N, arr[i]等于 0 表示str中i位置的字符不许修改, arr[i] 等于 ...
- 2021-08-12:约瑟夫环问题。给定一个链表头节点head,和一个正数m,从头开始,每次数到m就杀死当前节点 ,然后被杀节点的下一个节点从1开始重新数, 周而复始直到只剩一个节点,返回最后的节点。
2021-08-12:约瑟夫环问题.给定一个链表头节点head,和一个正数m,从头开始,每次数到m就杀死当前节点 ,然后被杀节点的下一个节点从1开始重新数, 周而复始直到只剩一个节点,返回最后的节点. ...
- vue全家桶进阶之路40:Vue3父件传值给子件
在Vue3中,可以通过props将父组件的数据传递给子组件.具体步骤如下: 在父组件中定义要传递给子组件的数据,可以是data属性中的数据或者是计算属性computed中的数据. 在子组件中通过pro ...
- 百度飞桨(PaddlePaddle) - PP-OCRv3 文字检测识别系统 Paddle Inference 模型推理
Paddle Inference 模型推理流程 分别介绍文字检测.方向分类器和文字识别3个模型,基于Paddle Inference的推理过程. Paddle Inference 的 Python 离 ...
- 2014年蓝桥杯C/C++大学B组省赛真题(蚂蚁感冒)
题目描述: 长100厘米的细长直杆子上有n只蚂蚁.它们的头有的朝左,有的朝右. 每只蚂蚁都只能沿着杆子向前爬,速度是1厘米/秒.当两只蚂蚁碰面时,它们会同时掉头往相反的方向爬行.这些蚂蚁中,有1只蚂蚁 ...
- itextpdf5.5.13给pdf添加图片水印、添加文字水印(平铺)、添加文字水印(单个)、添加页眉、页脚、页眉事件、添加图片
转载自简书用户:alex很累,感谢分享.原地址:https://www.jianshu.com/p/2b9c7a0300e4 一.相关工具类 1. Excel2Pdf.java (如代码不可用请查看原 ...
- cv学习总结(11.14-11.20)
本周主要完成了assignment2中的connected_layer部分的代码,跟assignment1中的two_layer_net相比,虽然整体思路都是实现全连接的网络,但是connect_la ...
- 一天吃透Spring面试八股文
内容摘自我的学习网站:topjavaer.cn Spring是一个轻量级的开源开发框架,主要用于管理 Java 应用程序中的组件和对象,并提供各种服务,如事务管理.安全控制.面向切面编程和远程访问等. ...