JavaScript面试:什么是Promises ?
什么是promise?
promise是将来可能会产生单个值的对象</mark>:已解析的值或未解析的原因(例如,发生网络错误)。一个promise可能处于以下三种可能状态之一:已实现,已拒绝或未决。promise用户可以附加回调以处理已实现的值或拒绝的原因。
promise的不完整历史
promise早在1980年代就开始以MultiLisp和Concurrent Prolog之类的语言出现。1988年,芭芭拉·里斯科夫(Barbara Liskov)和刘巴·斯里拉(Liuba Shrira)创造了“promise”一词。
我第一次听说JavaScript中的Promise时,Node是全新的,社区正在讨论处理异步行为的最佳方法。社区在一段时间内尝试了Promise,但最终决定采用Node-standard的错误优先回调。
大约在同一时间,Dojo通过Deferred API添加了promise。日益增长的兴趣和活动最终导致了新成立的Promises / A规范,旨在使各种promise更加可互操作。
jQuery的异步行为围绕promise进行了重构。jQuery的Promise支持与Dojo的Deferred有着惊人的相似之处,并且由于jQuery的巨大普及,它很快就成为JavaScript中最常用的Promise实现。但是,它不支持人们指望在promise之上构建工具的两个通道(已实现/已拒绝)链接行为和异常管理。
尽管存在这些弱点,jQuery正式使JavaScript Promise成为主流,并且更好的独立Promise库(如Q,When和Bluebird)变得非常流行。jQuery的实现不兼容促使在Promise规范中进行了一些重要的澄清,该规范被重写并重新命名为Promises / A +规范。
ES6在Promise全球范围内带来了Promises / A +兼容标准,并且在新的Promise标准支持之上构建了一些非常重要的API:值得注意的是WHATWG Fetch规范和Async Functions标准(在撰写本文时为第3阶段草案)。
此处描述的promise是与Promises / A +规范兼容的promise,重点是ECMAScript标准Promise实现。
promise如何运作
Promise是可以从异步函数同步返回的对象。它将处于以下三种可能状态之一:
- 实现: onFulfilled()将被调用(例如,resolve()被调用)
- 拒绝: onRejected()将被调用(例如reject()被调用)
- 待处理:尚未实现或被拒绝
如果promise不是挂起表示解决。
一旦解决,promise就无法重新执行。再次调用resolve()或reject()将无效。兑现promise的不变性是一个重要特征。
原生的JavaScriptpromise不会公开promise状态。相反,您应该将promise视为黑匣子。只有负责创建promise的职能部门才能了解promise状态,或者可以访问解决或拒绝。
这是一个返回promise的函数,该promise将在指定的时间延迟后解决:
const wait = time => new Promise((resolve) => setTimeout(resolve, time));
wait(3000).then(() => console.log('Hello!')); // 'Hello!'
我们的wait(3000)呼叫将等待3000毫秒(3秒),然后打印'Hello!'。所有与规范兼容的promise都定义了一种.then()方法,您可以使用该方法来传递可以采用已解析或拒绝值的处理程序。
ES6 Promise构造函数带有一个函数。这个函数有两个参数,resolve()和reject()。在上面的示例中,我们仅使用resolve(),所以我省略reject()了参数列表。然后,我们调用setTimeout()创建延迟,并在延迟resolve()完成时调用。
您可以选择使用resolve()或reject()使用值,这些值将传递给随附的回调函数.then()。
当我reject()有一个值时,我总是传递一个Error对象。通常,我想要两个可能的解决状态:正常的正确路径或异常-阻止正常的正确路径发生的任何事情。传递Error对象使这一点变得明确。
重要promise规则
Promises / A +规范社区定义了promise标准。有许多符合标准的实现,包括JavaScript标准ECMAScript Promise。
遵循规范的promise必须遵循一组特定的规则:
- Promise或“ thenable”是提供符合标准的
.then()方法的对象。 - 未完成的
promise可能会转换为已实现或已拒绝状态。 - 已兑现或被拒绝的
promise将得到解决,并且不得过渡到任何其他状态。 - 兑现
promise后,它必须具有一个值(可能是undefined)。该值不得更改。
在此上下文中的更改是指标识(===)比较。对象可以用作实现值,并且对象属性可能会发生变化。
每个promise都必须提供.then()具有以下签名的方法:
promise.then(
onFulfilled?: Function,
onRejected?: Function
) => Promise
该.then()方法必须符合以下规则:
- 这两个onFulfilled()和onRejected()是可选的。
- 如果提供的参数不是函数,则必须将其忽略。
- onFulfilled() 在实现
promise后将被调用,以promise的值作为第一个参数。 - onRejected()将在
promise被拒绝后被调用,拒绝的原因是第一个参数。原因可能是任何有效的JavaScript值,但是由于拒绝实质上是异常的同义词,因此我建议使用Error对象。 - 无论是onFulfilled()也onRejected()可以称为一次以上。
- .then()在相同的
promise下可能被多次呼唤。换句话说,promise可以用于聚集回调。 - .then()必须返回新的
promisepromise2。 - 如果onFulfilled()或onRejected()返回一个值x,并且x是一个
promise,promise2则将使用(假定与状态和值相同)锁定x。否则,promise2将满足的值x。 - 如果任何一个onFulfilled或onRejected引发异常e,则promise2必须e以其为理由予以拒绝。
- 如果onFulfilled不是函数且promise1已实现,则promise2必须使用与相同的值来实现promise1。
- 如果onRejected不是功能而promise1被拒绝,则promise2必须以与相同的理由将其拒绝promise1。
promise链
由于.then()总是返回新的promise,因此可以通过对错误的处理方式和位置进行精确控制来链接promise。promise允许您模仿普通的同步代码的try/catch行为。
像同步代码一样,链接将导致序列以串行方式运行。换句话说,您可以执行以下操作:
fetch(url)
.then(process)
.then(save)
.catch(handleErrors)
;
假设每个功能fetch(),process()以及save()回报promise,process()将等待fetch()到完全启动之前,并save()会等待process()至完全启动前。handleErrors()仅在任何先前的promise被拒绝的情况下运行。
这是带有多个拒绝的复杂promise链的示例:
const wait = time => new Promise(
res => setTimeout(() => res(), time)
);
wait(200)
// onFulfilled() can return a new promise, `x`
.then(() => new Promise(res => res('foo')))
// the next promise will assume the state of `x`
.then(a => a)
// Above we returned the unwrapped value of `x`
// so `.then()` above returns a fulfilled promise
// with that value:
.then(b => console.log(b)) // 'foo'
// Note that `null` is a valid promise value:
.then(() => null)
.then(c => console.log(c)) // null
// The following error is not reported yet:
.then(() => {throw new Error('foo');})
// Instead, the returned promise is rejected
// with the error as the reason:
.then(
// Nothing is logged here due to the error above:
d => console.log(`d: ${ d }`),
// Now we handle the error (rejection reason)
e => console.log(e)) // [Error: foo]
// With the previous exception handled, we can continue:
.then(f => console.log(`f: ${ f }`)) // f: undefined
// The following doesn't log. e was already handled,
// so this handler doesn't get called:
.catch(e => console.log(e))
.then(() => { throw new Error('bar'); })
// When a promise is rejected, success handlers get skipped.
// Nothing logs here because of the 'bar' exception:
.then(g => console.log(`g: ${ g }`))
.catch(h => console.log(h)) // [Error: bar]
;
错误处理
注意promise既有成功也有错误处理程序,看到这样做的代码很常见:
save().then(
handleSuccess,
handleError
);
但是如果handleSuccess()抛出错误怎么办?从中返回的promise.then()将被拒绝,但是没有任何东西可以解决该拒绝-这意味着您的应用程序中的错误会被吞噬.
因此,有些人认为上面的代码是反模式,因此建议以下代码:
save()
.then(handleSuccess)
.catch(handleError)
差异是微妙的,但很重要。在第一个示例中,save()将捕获起源于操作的错误,但是handleSuccess()将吞噬源自函数的错误。
如果没有.catch(),则不会捕获成功处理程序中的错误
在第二个示例中,.catch()将处理来自save()或的拒绝handleSuccess()。
使用.catch(),可以处理两个错误源
当然,该save()错误可能是网络错误,而该handleSuccess()错误可能是因为开发人员忘记处理特定的状态代码。如果您想以不同的方式处理它们怎么办?您可以选择同时处理它们:
save()
.then(
handleSuccess,
handleNetworkError
)
.catch(handleProgrammerError)
;
无论您喜欢什么,我建议都以结尾结尾所有promise链.catch()。值得重复:
我建议所有的
promise链都以结束.catch()。
我如何取消promise?
新的Promise用户经常想知道的第一件事就是如何取消Promise。这是一个主意:以“已取消”为理由拒绝promise。如果您需要以不同于“正常”错误的方式处理它,请在错误处理程序中进行分支。
这是人们在取消promise时会犯的一些常见错误:
将.cancel()添加到promise
添加.cancel()使promise成为非标准的,但是它也违反了promise的另一条规则:只有创建promise的函数才可以解析,拒绝或取消promise。暴露它会破坏这种封装,并鼓励人们在不了解它的地方编写操纵promise的代码。避免意大利面条和promise。
忘记清理
一些聪明的人发现,有一种方法可以Promise.race()用作取消机制。这样做的问题是取消控制是从创建promise的函数中获取的,这是您可以进行适当清理活动的唯一位置,例如清除超时或通过清除对数据的引用来释放内存等。
忘记处理被拒绝的取消promise
您是否知道当您忘记处理promise拒绝时,Chrome会在整个控制台上引发警告消息?
过于复杂
所述撤回TC39提案用于消除提出了用于取消一个单独的消息信道。它还使用了称为取消令牌的新概念。我认为,该解决方案会使promise规范大大膨胀,并且它提供的唯一功能是,推测不直接支持的是拒绝与取消的分离,而IMO则不必这样做。
您是否要根据异常还是取消进行切换?是的,一点没错。那是promise的工作吗?在我看来,不,不是。
重新考虑promise取消
通常,我会传递promise所需的所有信息,以确定在promise创建时如何解决/拒绝/取消。这样一来,就无需.cancel()在promise中使用任何方法。您可能想知道如何知道在promise创建时是否要取消。
“如果我还不知道是否要取消,那么在创建
promise时我将如何知道要传递什么?”
如果只有某种对象可以代表将来的潜在价值……哦,等等。
我们传递来表示是否取消的值本身就是一个promise。这可能是这样的:
const wait = (
time,
cancel = Promise.reject()
) => new Promise((resolve, reject) => {
const timer = setTimeout(resolve, time);
const noop = () => {};
cancel.then(() => {
clearTimeout(timer);
reject(new Error('Cancelled'));
}, noop);
});
const shouldCancel = Promise.resolve(); // Yes, cancel
// const shouldCancel = Promise.reject(); // No cancel
wait(2000, shouldCancel).then(
() => console.log('Hello!'),
(e) => console.log(e) // [Error: Cancelled]
);
我们正在使用默认参数分配来告诉它默认情况下不取消。这使得该cancel参数方便地是可选的。然后我们像以前一样设置超时,但是这次我们捕获了超时的ID,以便以后可以清除它。
我们使用该cancel.then()方法来处理取消和资源清理。只有在promise有机会被解决之前取消promise,此操作才会运行。如果您取消得太晚,则错过了机会。那列火车已经离开车站了。
注意:您可能想知道该noop()功能的用途。Noop一词代表无操作,表示不执行任何操作的功能。如果没有它,V8将引发警告:UnhandledPromiseRejectionWarning: Unhandled promise rejection。始终处理
promise拒绝是一个好主意,即使您的处理程序是noop()。
抽象promise取消
这对于wait()计时器来说很好,但是我们可以进一步抽象该思想以封装您必须记住的所有内容:
- 默认情况下拒绝取消
promise-如果没有传递取消promise,我们不想取消或抛出错误。 - 当您拒绝取消时,请记住执行清理。
- 请记住,onCancel清理本身可能会引发错误,并且该错误也需要处理。(请注意,在上面的等待示例中省略了错误处理,这很容易忘记!)
让我们创建一个可取消的Promise实用程序,可用于包装任何Promise。例如,处理网络请求等……签名将如下所示:
speculation(fn: SpecFunction, shouldCancel: Promise) => Promise
SpecFunction就像您将传递给Promise构造函数的函数一样,但有一个例外-它需要一个onCancel()处理程序:
SpecFunction(resolve: Function, reject: Function, onCancel: Function) => Void
// HOF Wraps the native Promise API
// to add take a shouldCancel promise and add
// an onCancel() callback.
const speculation = (
fn,
cancel = Promise.reject() // Don't cancel by default
) => new Promise((resolve, reject) => {
const noop = () => {};
const onCancel = (
handleCancel
) => cancel.then(
handleCancel,
// Ignore expected cancel rejections:
noop
)
// handle onCancel errors
.catch(e => reject(e))
;
fn(resolve, reject, onCancel);
});
请注意,此示例只是一个示例,旨在向您说明其工作原理。您还需要考虑其他一些极端情况。例如,在此版本中,handleCancel如果您已经兑现了promise,则将被调用。
我已经实现了此版本的维护生产版本,并在边缘案例中涵盖了开源库Speculation。
让我们使用改进的库抽象来重写wait()以前的cancellable实用程序。首先安装speculation:
npm install --save speculation
现在,您可以导入和使用它:
import speculation from 'speculation';
const wait = (
time,
cancel = Promise.reject() // By default, don't cancel
) => speculation((resolve, reject, onCancel) => {
const timer = setTimeout(resolve, time);
// Use onCancel to clean up any lingering resources
// and then call reject(). You can pass a custom reason.
onCancel(() => {
clearTimeout(timer);
reject(new Error('Cancelled'));
});
}, cancel); // remember to pass in cancel!
wait(200, wait(500)).then(
() => console.log('Hello!'),
(e) => console.log(e)
); // 'Hello!'
wait(200, wait(50)).then(
() => console.log('Hello!'),
(e) => console.log(e)
); // [Error: Cancelled]
这可以简化一些事情,因为您不必担心noop(),捕获onCancel()函数,函数或其他边缘情况下的错误。这些细节已被摘录speculation()。签出它,并随时在实际项目中使用它。
Native JS Promise的其他功能
原生的Promise对象还有一些您可能感兴趣的东西:
- Promise.reject() 返回被拒绝的
promise。 - Promise.resolve() 返回已解决的
promise。 - Promise.race() 接受一个数组(或任何可迭代的),并返回一个以迭代器中第一个已解决的promise的值进行解析的promise,或以第一个被拒绝的promise的原因拒绝。
- Promise.all()接受一个数组(或任何可迭代的)并返回一个promise,当可迭代参数中的所有promise都已解决时,该promise将解决;或者以第一个传递的promise拒绝为由拒绝。
结论
promise已成为JavaScript中许多习惯用法的组成部分,其中包括用于大多数现代ajax请求的WHATWG Fetch标准以及用于使异步代码看起来同步的Async Functions标准。
在撰写本文时,异步功能是第3阶段,但是我预测它们很快将成为JavaScript中异步编程非常流行,非常常用的解决方案-这意味着学习兑现promise对于JavaScript将会变得更加重要。开发者在不久的将来。
例如,如果您使用Redux,建议您检出redux-saga:一个用于管理Redux中副作用的库,该库依赖于整个文档中的异步功能。
我希望即使是经验丰富的promise用户也可以在阅读此书后更好地了解什么是promise,它们如何工作以及如何更好地使用它们。