JavaScript面试:什么是Promises ?

2021-02-21  本文已影响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是可以从异步函数同步返回的对象。它将处于以下三种可能状态之一:

如果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都必须提供.then()具有以下签名的方法:

promise.then(
  onFulfilled?: Function,
  onRejected?: Function
) => Promise

该.then()方法必须符合以下规则:

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。例如,处理网络请求等……签名将如下所示:

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已成为JavaScript中许多习惯用法的组成部分,其中包括用于大多数现代ajax请求的WHATWG Fetch标准以及用于使异步代码看起来同步的Async Functions标准。

在撰写本文时,异步功能是第3阶段,但是我预测它们很快将成为JavaScript中异步编程非常流行,非常常用的解决方案-这意味着学习兑现promise对于JavaScript将会变得更加重要。开发者在不久的将来。

例如,如果您使用Redux,建议您检出redux-saga:一个用于管理Redux中副作用的库,该库依赖于整个文档中的异步功能。

我希望即使是经验丰富的promise用户也可以在阅读此书后更好地了解什么是promise,它们如何工作以及如何更好地使用它们。

参考

Master the JavaScript Interview: What is a Promise?

上一篇 下一篇

猜你喜欢

热点阅读