深度理解 ES6 Promise
现在我们开始 ES6—— 深度系列。 如果大家以前从未来到过这里,请从 ES6 教程开始。 这篇是关于 ES6 中的 Promise API。
如果觉得该动画非常复杂,请继续阅读!
Promises 是一个非常复杂的范例,所以我们慢慢来。
这是一个目录,其中包含我们将在本文中介绍的主题。
-
什么是 Promise ? – 我们定义 Promise 并查看 JavaScript 中的一个简单示例
- 回调和事件——处理异步代码流的替代方法
- Promise 的要点——初识 Promise 是如何工作的
- Promises in Time——Promises 的简史
- 对 .then 和 .catch 的分析
- 从头开始创建 Promise
- 解决 Promise ——讨论 Promise 的状态
- 用另一个 Promise 兑现一个 Promise——解释 promise 链
- Transforming Values in Promises——展示如何在 promises 的上下文中将结果转化为其他东西
- 利用 Promise.all 和 Promise.race
下面我们逐一介绍
什么是 Promise ?
Promises 通常被模糊地定义为“最终将可用的值的代理”。 它们可以用于同步和异步代码流,尽管它们使异步流更容易推理——一旦你掌握了承诺,那就是。
以 fetch API 为例。 此 API 是 XMLHttpRequest
的简化版。 它旨在超级简单地用于最基本的用例:通过 http(s) 对与当前页面相关的资源发出 GET 请求——它还提供了一个全面的 API 来满足高级用例,但那是 暂时不是我们的重点。 在最基本的化身中,我们可以像这样发出 GET foo 请求。
fetch('foo')
fetch('foo')
语句似乎并不那么令人兴奋。 它针对 foo 相对于我们当前使用的资源发出“即发即弃”的 GET 请求。 fetch
方法返回一个 Promise。 我们可以链接一个 .then
回调,该回调将在 foo 资源完成加载后执行。
fetch('foo').then(response => /* do something */)
Promises 提供了回调和事件的替代方案。
回调和事件
如果获取 API 使用回调,我们将获得最后一个参数,然后在获取结束时执行该参数。 典型的异步代码流约定规定我们在获取过程中为错误(可能发生也可能不发生)分配第一个参数。 其余参数可用于传递结果数据。 通常,使用单个参数。
fetch('foo', (err, res) => {
if (err) {
// handle error
}
// handle response
})
在获取 foo 资源之前不会调用回调,因此它的执行保持异步和非阻塞。
请注意
,在此模型中,我们只能指定一个回调,并且该回调将负责从响应派生的所有功能。
另一种选择可能是使用事件驱动的 API 模型。 在这个模型中,fetch
返回的对象将能够监听 .on
事件,为任何事件绑定尽可能多的事件处理程序。 通常,当出现问题时会有一个错误事件,当操作成功完成时会调用一个数据事件。
fetch('foo')
.on('error', err => {
// handle error
})
.on('data', res => {
// handle response
})
在这种情况下,如果没有附加事件侦听器,错误通常会以硬异常结束——但这取决于使用的事件发射器实现。 承诺有点不同。
promises 的要点
promises 不是通过 .on
绑定事件侦听器,而是提供了一个略有不同的 API。 下面显示的代码片段显示了 fetch
方法的实际 API,它返回一个 Promise 对象。 与事件非常相似,我们可以使用 .catch
和 .then
绑定任意数量的侦听器。
请注意
,使用 promises 的声明性方法不再需要事件类型。
var p = fetch('foo')
p.then(res => {
// handle response
})
p.catch(error => {
// handle error
})
还要注意 .then
能够将对拒绝的反应注册为它的第二个参数。 上面可以表示为下面的一段代码。
fetch('foo')
.then(
res => {
// handle response
},
err => {
// handle error
}
)
就像你可以省略 .then(fulfillment)
中的错误反应一样,你也可以省略对 fulfillment 的反应。 使用 .then(null, rejection)
等同于 .catch(rejection)
。
请注意
,.then
和.catch
每次都会返回一个新的 Promise。 这很重要,因为链可能会产生截然不同的结果,具体取决于你在何处附加.then
或.catch
调用。 请参阅以下示例以了解差异。
fetch('foo').then(res => {}, error => {})
var p = fetch('foo')
p.then(res => {}, error => {})
var p2 = fetch('foo')
p2.then(res=>{})
p2.catch(error => {})
fetch('foo').then(res=>{}).catch(error=>{})
稍后我们将更深入地了解这两种方法。 在这样做之前,让我们先看一下 promise 的简要历史。
promise 的简要历史
promise 并不是那么新鲜。 与计算机科学中的大多数事物一样,最早提到 Promises 可以追溯到七十年代末。 根据 Internet,它们于 2007 年首次出现在 JavaScript 中——在一个名为 MochiKit 的库中。 然后 Dojo 采用了它,紧接着 jQuery 也采用了它。
然后 Promises/A+ 规范来自 CommonJS 组(现在以其 CommonJS 模块规范而闻名)。 在其最早的版本中,Node.js 附带了 promises。 一段时间后,它们从核心中移除,每个人都切换到回调。 现在,promise 随 ES6 标准一起发布,并且 V8 已经在一段时间前实现了它们。
ES6 标准原生实现了 Promises/A+。 在最新版本的 Node.js 中,我们可以在没有任何库的情况下使用 promises。 它们也适用于 Chrome 32+、Firefox 29+ 和 Safari 7.1+。
我们要回到 Promise API 吗?
对 .then 和 .catch 的分析
回到我们的例子——这是我们的一些代码。 在最简单的用例中,这就是我们想要的。
fetch('foo').then(res => {
// handle response
})
如果传递给 .then 的反应之一发生错误怎么办? 我们可以使用 .catch
捕获它们。 下面代码片段中的示例记录了尝试从 res 中未定义的 a 属性访问 prop 时捕获的错误。
fetch('foo')
.then(res => res.a.prop.that.does.not.exist)
.catch(err => console.error(err.message))
// <- 'Cannot read property "prop" of undefined'
请注意
,我们将反应放在什么地方很重要。 下面的例子不会打印err.message
两次——只打印一次。 那是因为第一个 .catch 中没有发生错误,所以那个 promise 的拒绝分支没有被执行。
fetch('foo')
.then(res => res.a.prop.that.does.not.exist)
.catch(err => console.error(err.message))
.catch(err => console.error(err.message))
// <- 'Cannot read property "prop" of undefined'
相反,下面的代码片段将打印 err.message
两次。 它的工作原理是保存对 .then 返回的 promise 的引用,然后在其上添加两个 .catch
反应。 前面示例中的第二个 .catch
是捕获从第一个 .catch
返回的 promise 中产生的错误,而在本例中,两个 .catch
都从 p 分支出来。
var p = fetch('foo').then(res => res.a.prop.that.does.not.exist)
p.catch(err => console.error(err.message))
p.catch(err => console.error(err.message))
// <- 'Cannot read property "prop" of undefined'
// <- 'Cannot read property "prop" of undefined'
这是另一个突出这种差异的例子。 这次触发了第二个 catch,因为它绑定到第一个 .catch
上的拒绝分支。
fetch('foo')
.then(res => res.a.prop.that.does.not.exist)
.catch(err => { throw new Error(err.message) })
.catch(err => console.error(err.message))
// <- 'Cannot read property "prop" of undefined'
如果第一个 .catch
调用没有返回任何内容,则不会打印任何内容。
fetch('foo')
.then(res => res.a.prop.that.does.not.exist)
.catch(err => {})
.catch(err => console.error(err.message))
// nothing happens
那么,我们应该观察到,promises 可以“任意”链接,也就是说:正如我们刚才看到的,我们可以保存对 promise 链中任何点的引用,然后在其上添加更多 promises。 这是理解 promise 的基本要点之一。
我们可以保存对 promise 链中任何点的引用。
实际上,最后一个示例可以表示为如下所示。 这段代码让我们更容易理解到目前为止所讨论的内容。 浏览一下,然后我们介绍一些要点。
var p1 = fetch('foo')
var p2 = p1.then(res => res.a.prop.that.does.not.exist)
var p3 = p2.catch(err => {})
var p4 = p3.catch(err => console.error(err.message))
有一些要点。
- fetch 返回一个全新的 p1 promise
- p1.then 返回一个全新的 p2 promise
- p2.catch 返回一个全新的 p3 promise
- p3.catch 返回一个全新的 p4 promise
- 当 p1 被解决(完成)时,执行 p1.then 反应
- 在那之后,正在等待 p1.then 的未决结果的 p2 被结算
- 由于 p2 被拒绝,p2.catch 反应被执行(而不是 p2.then 分支)
- 来自 p2.catch 的 p3 promise 已实现,即使它没有产生任何值或错误
- 因为 p3 成功了,p3.catch 永远不会执行—— p3.then 分支会被使用
我们应该将 promises 视为树结构。 这一切都始于一个单一的承诺,我们稍后会看到如何构建它。 然后使用 .then
或 .catch
添加一个分支。 我们可以根据需要在每个分支上附加任意数量的 .then
或 .catch
调用,创建新分支,等等。
从头开始创建 promise
我们现在应该了解 promise 是如何像树一样工作的,我们可以根据需要在需要的地方添加分支。 但是你如何从头开始创建一个承诺呢? 编写这类 Promise 教程很困难,因为这是先有鸡还是先有蛋的情况。 人们几乎不需要从头开始创建 promise,因为类库通常会处理这些事情。 例如,在这篇文章中,我特意开始使用 fetch 来解释事情,它在内部创建了一个新的 promise 对象。 然后,对 fetch 创建的 promise 的每次调用 .then
或 .catch
也会在内部创建一个promise,并且在决定是否应执行履行分支或拒绝分支时,这些 promise 取决于它们的父级。
可以使用 new Promise(resolver)
从头开始创建 Promise。 resolver 参数是将用于解析 promise 的方法。 它有两个参数,一个 resolve 方法和一个 reject 方法。 这些 promises 分别在下一个 tick 时实现和拒绝——如 Promisee 所示。
new Promise(resolve => resolve()) // promise is fulfilled
new Promise((resolve, reject) => reject()) // promise is rejected
不过,解决和拒绝没有价值的 promise 并不是那么有用。 通常 promises 会解决一些结果,比如我们在 fetch 中看到的来自 AJAX 调用的响应。 同样,我们可能想要说明拒绝的原因——通常使用 Error 对象。 下面的代码整理了我们刚刚阅读的内容。
new Promise(resolve => resolve({ foo: 'bar' }))
.then(result => console.log(result))
// <- { foo: 'bar' }
new Promise((resolve, reject) =>
reject(new Error('failed to deliver on my promise to you')))
.catch(reason => console.log(reason))
// <- Error: failed to deliver on my promise to you
正如我们可能已经猜到的那样,promises 本身并没有什么同步的。 履行和拒绝都可以完全异步。 这就是 promise 的全部意义! 下面的 promise 在两秒后完成。
new Promise(resolve => setTimeout(resolve, 2000))
重要的是要注意,只有对这些方法中的任何一个进行的第一次调用才会产生影响——一旦 Promise 得到解决,它的结果就不能改变。 下面的示例创建了一个 Promise,该 Promise 在分配的时间内完成或在大量超时后被拒绝。
function resolveUnderThreeSeconds (delay) {
return new Promise(function (resolve, reject) {
setTimeout(resolve, delay)
setTimeout(reject, 3000)
})
}
resolveUnderThreeSeconds(2000) // resolves!
resolveUnderThreeSeconds(7000) // fulfillment took so long, it was rejected.
除了返回解析值外,我们还可以使用另一个 promise 进行解析。 在这些情况下会发生什么? 在下面的代码片段中,我们创建了一个将在三秒内被拒绝的 promise p
。 我们还创建了一个 promise p2
,它将在一秒钟内用 p
解决。 由于 p
还差两秒,解决 p2
不会立即生效。 两秒后,当 p
被拒绝时,p2
也将被拒绝,拒绝原因与提供给 p
的相同。
var p = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
var p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p), 1000)
})
p2.then(result => console.log(result))
p2.catch(error => console.log(error))
// <- Error: fail
在下面显示的动画中,我们可以观察到 p2
是如何被阻塞的——用黄色标记——等待 p
中的结算。
请注意
,此行为仅适用于使用resolve
的实现分支。 如果你尝试使用reject
复制相同的行为,会发现p2
promise 只是被拒绝,而p
promise 作为拒绝原因。
使用 Promise.resolve 和 Promise.reject
有时你想创建一个 Promise,但又不想经历使用构造函数的麻烦。 以下语句创建一个以“foo”结果实现的 Promise。
new Promise(resolve => resolve('foo'))
如果你已经知道应履行 Promise 的价值,则可以改用 Promise.resolve
。 下面的语句等同于前面的语句。
Promise.resolve('foo')
同样,如果你已经知道拒绝原因,你可以使用 Promise.reject
。 下一个语句创建了一个 Promise,该 Promise 将有理由地被拒绝。
Promise.reject(reason)
关于兑现 Promise,我们还应该了解什么?
达成 Promise
Promise 可以存在三种状态:pending
、fulfilled
和 rejected
。 挂起是默认状态。 从那里,Promise 可以“确定”为实现或拒绝。 一旦一个 Promise 被解决,所有等待它的反应都会被评估。 那些在正确分支上的—— .then
用于实现,.catch
用于拒绝——被执行。
从这一刻起,Promise 就敲定了。 如果在稍后的某个时间点将另一个反应链接到已确定的 Promise 上,则该反应的适当分支将在程序的下一个时钟中执行。 在下面的示例中,p 在两秒后解析为值 100。 然后,100 被打印到屏幕上。 两秒后,另一个 .then
分支被添加到 p 上,但由于 p 已经完成,新分支立即被执行。
var p = new Promise(function (resolve, reject) {
setTimeout(() => resolve(100), 2000)
})
p.then(result => console.log(result))
// <- 100
setTimeout(() => p.then(result => console.log(result * 20)), 4000)
// <- 2000
一个 promise 可以返回另一个 promise——这就是启用和支持大多数异步行为的原因。 在上一节中,当从头开始创建一个 promise 时,我们看到我们可以用另一个 promise 来解决。 我们也可以在调用 .then
时返回 promise。
用另一个 Promise 兑现一个 Promise
下面的例子展示了我们如何使用一个 Promise,然后是另一个只有在返回的 Promise 也结算后才会结算的 Promise。 一旦发生这种情况,我们就会从包装的 promise 中得到响应,然后我们使用 res.url
来找出我们被赋予了哪些随机文章。
fetch('foo')
.then(response => fetch('/articles/random'))
.then(response => console.log(response.url))
// <- 'http://ponyfoo.com/articles/es6-symbols-in-depth'
显然,在现实世界中,我们的第二次提取可能取决于第一次提取的响应。 这是返回承诺的另一个例子,我们在一秒钟后随机履行或拒绝。
var p = Promise.resolve()
.then(data => new Promise(function (resolve, reject) {
setTimeout(Math.random() > 0.5 ? resolve : reject, 1000)
}))
p.then(data => console.log('okay!'))
p.catch(data => console.log('boo!'))
这个动画超级好玩!
好吧,这不是那么有趣。 我在制作 Promisees 工具本身时确实很开心!
转换 Promise 中的值
我们不仅限于从 .then
和 .catch
回调中返回其他 Promise。 我们还可以返回值,转换我们拥有的内容。 下面的示例首先创建一个用 [1, 2, 3]
实现的 Promise ,然后在将这些值映射到 [2, 4, 6]
的顶部有一个实现分支。 在 promise 的那个分支上调用 .then
将产生双倍的值。
Promise.resolve([1, 2, 3])
.then(values => values.map(value => value * 2))
.then(values => console.log(values))
// <- [2, 4, 6]
请注意,我们可以在拒绝分支中执行相同的操作。 一个可能引起我们注意的有趣事实是,如果 .catch 分支顺利进行且没有错误,那么它将用返回值实现。 这意味着如果我们仍然希望该分支有错误,应该再次抛出。 下面的一段代码接受了一个内部错误,并将其隐藏在一个通用的“内部服务器错误”消息后面,以免向其客户端泄露潜在的危险信息(可视化)。
Promise.reject(new Error('Database ds.214.53.4.12 connection timeout!'))
.catch(error => { throw new Error('Internal Server Error') })
.catch(error => console.info(error))
// <- Error: Internal Server Error
映射 Promise 结果在处理多个并发 Promise 时特别有用。 让我们看看它是什么样的。
利用 Promise.all 和 Promise.race
一个非常常见的场景——对于那些习惯使用 Node.js 的人来说更是如此——是在能够做 C 之前依赖于 A 和 B。我将继续使用多个代码片段对该场景进行糟糕的描述。
request('https://google.com', function (err, goog) {
request('https://twitter.com', function (err, twit) {
console.log(goog.length, twit.length)
})
})
回调地狱! 为什么我们要等谷歌的回应,然后再撤回 Twitter 的回应? 下面的部分解决了这个问题。 不过,它也长得离谱,对吧?
var results = {}
request('https://google.com', function (err, goog) {
results.goog = goog
done()
})
request('https://twitter.com', function (err, twit) {
results.twit = twit
done()
})
function done () {
if (Object.keys(results).length < 2) {
return
}
console.log(results.goog.length, results.twit.length)
}
由于没有人愿意编写那样的代码,因此像 async
和 contra
这样的实用程序库可以让你的代码更短。 我们可以使用 contra.concurrent
同时运行这些方法,并在它们全部结束后执行回调。 这就是它的样子。
contra.concurrent({
goog: function (next) {
request('https://google.com', next)
}
twit: function (next) {
request('https://twitter.com', next)
}
}, function (err, results) {
console.log(results.goog.length, results.twit.length)
})
对于非常常见的“我只想要一个在末尾附加那个神奇的 next 参数的方法”用例,还有 contra.curry
(相当于 async.apply)来使代码更短。
contra.concurrent({
goog: contra.curry(request, 'https://google.com'),
twit: contra.curry(request, 'https://twitter.com')
}, function (err, results) {
console.log(results.goog.length, results.twit.length)
})
Promises 已经使“在系列中的其他东西之后运行这个”用例变得非常简单,使用 .then
正如我们在前面的几个例子中看到的那样。 对于“同时运行这些东西”用例,我们可以使用 Promise.all。
Promise.all([
fetch('/'),
fetch('foo')
])
.then(responses => responses.map(response => response.statusText))
.then(status => console.log(status.join(', ')))
// <- 'OK, Not Found'
请注意
,即使拒绝了单个依赖项,Promise.all
方法也将被完全拒绝。
Promise.all([
Promise.reject(),
fetch('/'),
fetch('foo')
])
.then(responses => responses.map(response => response.statusText))
.then(status => console.log(status.join(', ')))
// nothing happens
总之,Promise.all
有两种可能的结果。
- 一旦其依赖项之一被拒绝,就以单一拒绝原因解决
- 一旦满足其所有依赖项,立即结算所有实现结果
然后是 Promise.race
。 这是一个类似于 Promise.all
的方法,除了第一个解决的 Promise 将“赢得”竞争,并且它的价值将传递给竞争的分支。 如果我们多次运行以下代码的可视化,我们会注意到这场竞争没有明显的赢家。 这取决于服务器和网络!
Promise.race([
fetch('/'),
fetch('foo')
])
.then(response => console.log(response.statusText))
// <- 'OK', or maybe 'Not Found'.
拒绝也将结束比赛,比赛 promise 将被拒绝。 作为结束语,我们可能会指出这对于我们想要超时我们无法控制的承诺的场景很有用。 例如,下面的比赛确实有意义。
var p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
])
p.then(response => console.log(response))
p.catch(error => console.log(error))
为了结束这篇文章,我会给你一个形象化的印象。 它显示了资源和超时之间的竞争,如上面的代码所示。
希望我没有做出让你更难理解的 Promise!
相关文章
在 TypeScript 中返回一个 Promise
发布时间:2023/03/19 浏览次数:182 分类:TypeScript
-
本教程讨论如何在 TypeScript 中返回正确的 Promise。这将提供 TypeScript 中 Returns Promise 的完整编码示例,并完整演示每个步骤。
在 JavaScript 中等待 Promise 得到解决
发布时间:2023/03/10 浏览次数:288 分类:JavaScript
-
在本文中,我们将研究如何在执行 JavaScript 中的下一部分代码之前等待 Promise 得到解决。
在 ES6 中使用 Node JS 创建和导出类
发布时间:2023/03/06 浏览次数:82 分类:WEB前端
-
类 是在 Javascript 中创建对象的蓝图。在新的语言规范 ECMAScript 2015 中,使用关键字 class 和 constructor() 方法创建类。 在这些新开发之前,开发人员依靠构造函数来创建面向对象的设计。
在 TypeScript 中声明一个 ES6 Map
发布时间:2023/03/05 浏览次数:199 分类:WEB前端
-
本篇文章将指导我们使用编码示例在 TypeScript 中定义 ES6 Map。 这解释了什么是 ES6 Map及其用途。 让我们首先看看 ES6 是什么以及为什么使用它们。 ES6 Map 在 ES6 之前,我们使用对象通过将
如何在 JavaScript ES6+ 中实现单例模式
发布时间:2023/02/08 浏览次数:197 分类:算法
-
在这篇文章中,我们将向大家展示如何在 JavaScript 中实现单例模式。 如果我们是一名全栈 JavaScript 开发人员,就会知道 JavaScript 是一种功能强大的语言,可以使用它来构建令人惊叹的网
异步堆栈跟踪:为什么 await 击败了 Promise#then()
发布时间:2023/01/09 浏览次数:75 分类:学无止境
-
与直接使用 promises 相比, async 和 await 不仅可以使开发人员的代码更具可读性它们还可以在 JavaScript 引擎中实现一些有趣的优化! 这篇文章是关于这样一种优化,涉及异步代码的堆栈跟
JavaScript 中检查函数是否返回的 Promise
发布时间:2022/12/15 浏览次数:557 分类:JavaScript
-
要检查函数是否返回 Promise ,请检查函数是否异步或调用它并检查函数是否返回具有函数类型 then 属性的对象。 如果满足任一条件,该函数将返回一个 Promise 。 // ✅ Promise check functio
JavaScript 中 Promise.resolve is not a constructor 错误
发布时间:2022/12/02 浏览次数:213 分类:JavaScript
-
当我们尝试将 Promise.resolve() 方法与 new 运算符一起使用时,会出现Promise.resolve is not a constructor错误。 Promise.resolve() 方法不是构造函数,因此应该在没有 new 运算符的情况下使用它,例如
JavaScript 中如何访问 Promise 的值
发布时间:2022/12/02 浏览次数:76 分类:JavaScript
-
使用 Promise.then() 方法访问承诺的值,例如 p.then(value = console.log(value)) 。 then() 方法接受一个函数,该函数将 promise 的解析值作为参数传递。 // ?️ Example promise const p = Promise . resolve (