美文网首页
JavaScript面试:什么是Promises ?

JavaScript面试:什么是Promises ?

作者: 魂斗驴 | 来源:发表于2021-02-21 11:31 被阅读0次

什么是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,因此可以通过对错误的处理方式和位置进行精确控制来链接promisepromise允许您模仿普通的同步代码的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,它们如何工作以及如何更好地使用它们。

参考

Master the JavaScript Interview: What is a Promise?

相关文章

网友评论

      本文标题:JavaScript面试:什么是Promises ?

      本文链接:https://www.haomeiwen.com/subject/jimtfltx.html