[译] 玩转 JavaScript 面试:何为 Promise ?

原文连接 Medium - Master the JavaScript Interview: What is a Promise?javascript

开门见山,何为 Promise ?

一个promise指的是一个可能会在将来的某个时间点产生一个单一值的对象:不管是一个 resolved 值,仍是一个未 resolved 值的缘由(好比发生了网络错误)。一个promise可能为fulfilledrejectedpending三种状态中的一种。promise用户可使用回调函数来处理fulfilledrejected状态。java

Promise能够说是至关热心了,其构造器一旦被调用,promise就会当即开始作你给它的任何任务。git

一个不太完整的 promise 发展史

早期对promisefutures(两个概念相似/相关)的实现始于如 MultiLispConcurrent Prolog语言于 20 世纪 80年代早期的出现。promise一词的使用是由 Barbara Liskov 和 Liuba Shrira 在 1988 年创造出来的1github

我第一次在 JavaScript 中知道promise这个概念时,Node才刚刚出现,当时的社区也在积极的讨论实现异步行为的最佳方式。在一段时间里,社区使用过promises这个概念,但最终落实在了标准Node错误处理回调上。promise

几乎在同一时期,Dojo框架中经过Deferred API添加了promises。随着公众对此持续不断兴趣和活跃度的高涨,最终造成了一个新的promise/A规范使得多种promises的实现得以统一。网络

jQuery 的异步行为围绕 promises 被重构。jQuery 对 promise 的支持与 Dojo 的 Deferred极其相似,也很快因其大规模受众而成为 JavaScript 中最受欢迎的 promise 实现方式。然而,jQuery 不支持fulfilled/rejected两个通道的链式调用行为和异常处理,而这些特性也是用户赖以使用 promise 构建应用的基础。app

尽管 jQuery 存在上述缺点,其依然成为当时 JavaScript promises 的主流实现,受喜好程度遥遥领先于像是QWhen或者Bluebird这样的 promise 库。jQuery 实现的不兼容性催生了一些对 promise 的补充说明,从而造成了Promises/A+规范。框架

ES6 中的 Promise 带来了对 Promises/A+ 规范的彻底兼容,另外还有一些很是重要的 API 也基于新的标准 Promise 而获得支持:最多见的有WHATWG Fetch规范和异步函数标准。异步

这里说明了 promises 是符合 Promises/A+ 规范的,也是 ECMAScript 标准Promise的实现。函数

Promise 是如何工做的

promise 是一个能够从一个异步函数中返回的异步对象,它能够处于如下三种状态中的一种:

  • Fulfilled:onFulfilled()会被调用(好比resolve()被调用)
  • Rejected:onRejected()会被调用(好比reject()被调用)
  • Pending: 暂时还未fulfilledrejected

promise 的状态只要不是 pending 即表明其已肯定状态(resolved 或 rejected),有时人们会用 resolved 和 settled 来表示同一个意思:非 pending状态。

状态一旦肯定,promise 的状态就不能再被改变,调用 resolve()reject()也不会产生任何影响。一个已肯定状态的 promise 的不可变性是其一大重要特性。

原生 JS promise 不对外暴露状态。实际上你可能更但愿把它当作一个黑盒机制来看待。只有当某函数的做用是建立一个 promise 时、或者是去访问 resolve 或 reject 时咱们才须要深刻 promise 的状态。

下面的函数会在指定时间后 resolve,而后返回一个 promise:

const wait = time => new Promise((resolve) => setTimeout(resolve, reject))

wait(3000).then(() => console.log('Hello!'));
复制代码

这里调用wait(3000)会在等待 3000ms 后打印出 Hello!。全部符合标准的 promises 都会定义一个.then()方法,可向该方法中传递一个句柄,从而拿到 resolve 或 reject 的值。

ES6 的 promise 构造函数接收一个函数做为参数。该函数接收两个参数分别为resolve()reject()。在上面的例子中,咱们只用到了resolve(),而后调用了setTimeout()建立一个延迟函数,最后在延迟函数执行完后调用resolve()

你能够选择只传给resolve()reject()一个值,值会被传递到.then()中的回调函数中。

每当我向reject()中传一个值时,我都会传一个 Error 对象进去。通常来讲我指望两种解决状态:正常的圆满结局,或者是抛出一个异常。传一个 Error 对象进去会使得结果更明朗。

几个重要的 Promise 规则

promises 的标准已经由Promises/A+ 规范社区定义好了。现存的不少种实现都遵照该规范,这其中就包括 JavaScript 标准 ECMAScript promises。

遵循上述规范的 promises 必须包含如下几点规则:

  • 一个 promise/thenable 对象必须提供一个标准的兼容的 .then()方法;
  • 处于 pending 状态的 promise 能够改成 fulfilled 或 rejected 两种状态;
  • 一个处于 fulfilled 或 rejected 中的 promise 状态一旦肯定,就不再能改变为其余状态;
  • 一旦 promise 状态肯定下来,它必须有一个值(即便是 undefined)。该值不可被改变。

每一个 promise 必须提供一个具有以下特性的.then()方法:

promise.then(
  onFulfilled?: Function,
  onRejected?: Function
) => Promise
复制代码

.then()方法必须符合下面的规则:

  • onFulfilled()onRejected()皆为可选参数;
  • 若是所提供的参数不是函数,参数将被忽略;
  • onFulfilled()会在 promise 的状态变为 fulfilled 时调用,promise 返回的值会被做为第一个参数;
  • onRejected()会在 promise 的状态变为 rejected 时调用,被拒绝的缘由会被做为第一个参数。缘由可能会是任何有效的 JavaScript 值,可是因为被拒绝基本上等同于抛出异常,因此我建议使用 Error 对象;
  • onFulfilled()onRejected()都不会被屡次调用;
  • .then()可能会在同一个 promise 上被调用屡次。换句话说,promise 可被用来合并回调函数;
  • .then()必须返回一个新的 promise,可称之为promise2
  • 若是onFulfilled()onRejected()返回一个值为xx是一个 promise,promise2将用x锁定。不然,promise2会被值x fulfilled。
  • 若是onFulfilled()onRejected()抛出一个异常epromise2必须以e做为缘由被 rejected;
  • 若是onFulfilled()不是函数,promise1被 fulfilled,那么promise2必须以相同的值被 fulfilled;
  • 若是onRejected()不是函数,promise1被 rejected,那么promise2必须以相同的缘由被 rejected;

Promise 链式调用

因为.then()老是返回一个新的 promise,这样就能够实现对链式 promise 中的错误进行精确控制。Promises 容许咱们模仿正常的同步代码行为(如 try...catch)。

就像同步代码同样,链式调用能够产生顺序执行的效果。好比下面这样:

fetch(url)
  .then(process)
  .then(save)
  .catch(handleErrors)
;
复制代码

假设上面的fetch()process()save()都返回 promises,process()会等待fetch()执行完毕后再开始执行,同理save()也要等待process()执行完毕才开始执行,handleErrors()当且仅当前面的任何一个 promises 运行出错才会执行。

下面给出一个复杂的例子:

const wait = time => new Promise(
  res => setTimeout(() => res(), time)
);

wait(200)
  // onFulfilled() 能够返回一个新的 promise, `x`
  .then(() => new Promise(res => res('foo')))
  // 下一个 promise 会假设 `x`的状态
  .then(a => a)
  // 上面咱们返回了未被包裹的`x`的值
  // 所以上面的`.then()`返回了一个 fulfilled promise
  // 有了上面的值以后:
  .then(b => console.log(b)) // 'foo'
  // 须要注意的是 `null` 是一个有效的 promise 返回值:
  .then(() => null)
  .then(c => console.log(c)) // null
  // 至此还未报错:
  .then(() => {throw new Error('foo');})
  // 相反, 返回的 promise 是 rejected
  // error 的缘由以下:
  .then(
    // 因为上面的 error致使在这里啥都没打印:
    d => console.log(`d: ${ d }`),
    // 如今咱们处理这个 error (rejection 的缘由)
    e => console.log(e)) // [Error: foo]
  // 有了以前的异常处理, 咱们能够继续:
  .then(f => console.log(`f: ${ f }`)) // f: undefined
  // 下面的代码未打印任何东西. e 已经被处理过了,
  // 因此该句柄并未被调用:
  .catch(e => console.log(e))
  .then(() => { throw new Error('bar'); })
  // 当一个 promise 被 rejected, success 句柄就被跳过.
  // 这里由于 'bar' 异常而不打印任何东西:
  .then(g => console.log(`g: ${ g }`))
  .catch(h => console.log(h)) // [Error: bar]
;
复制代码

错误处理

须要注意的是 promise 同时具备成功和失败的句柄,因此下面代码的写法很常见:

save().then(
    handleSuccess,
    handleError
)
复制代码

可是若是 handleSuccess()出错了怎么办?从.then()中返回的 promise 就会被 rejected,可是后续就没有能捕获该错误信息的函数了 —— 意思就是你 app 中的一个错误被吞掉了,这可有点儿糟糕。

针对上述缘由,有人就将上面的代码称为一种反模式(anti-pattern),并建议使用以下写法替代:

save()
    .then(handleSuccess)
    .catch(handleError);
复制代码

其中的差别很微妙,但却很重要。在头一个例子中,来自save()中的错误会被捕获,可是来自handleSuccess()中的错误就会被吞掉。

在第二个例子中, .catch()会处理来自不管是 save()仍是 handleSuccess()中的错误。
固然了,来自于 save()的错误还有多是网络错误,而 handleSuccess()中的错误可能来自于开发者忘记处理一个错误的状态码,要是你想对这两种错误进行不一样的处理该怎么办?那就能够选择下面这种处理方式了:

save()
    .then(
        handleSuccess,
        handleNetworkError
    )
    .catch(handleProgrammerError)
复制代码

不管你倾向于哪一种方式,我都推荐你在全部的 promises 后面带上 .catch()

要如何取消/中断一个 Promise

刚学会使用 promise 的用户老是有不少疑问,其中最多的就是关于如何取消/中断一个 promise。思路是这样的:直接去 reject 想要取消/中断的 promise,缘由就写「Cancelled」便可。但若是你要将它与常规错误处理方式区分开来的话,那就去开发本身的错误处理分支。

下面列出了几种人们在写取消/中断 promise 时常犯的错误:

给 promise 添加了一个.cancel()

添加.cancel()使得 promise 非标准化了,同时也违背了 promise 的另外一个规定:只有建立了 promise 的函数才有能力去 resolve、reject 或 取消/中断该 promise。传播这种写法只会破坏函数的封装特性,怂恿人们在不恰当的地方操做 promise 代码,破坏了 promise。

忘记清理

有些聪明的人搞清楚了使用promise.race()的方式来取消/中断 promise。这种方式的问题在于中断控制的操做是由建立该 promise 的函数发起的,这也是惟一一处恰当的进行清理动做的位置,好比说清理定时器或者经过解除对数据的引用来释放内存等等。

忘记处理一个被 reject 的中断 promise

你知道当你忘记处理一个 promise 的拒绝状态时 Chrome 抛出的满控制台的警告信息吗?

从新思考 promise 取消/中断

通常来讲,我会在一个 promise 建立时就把 promise 全部须要的信息都传给它,以便 promise 决定如何进行 resolve/reject/cancel。这种方式并不须要一个 .cancel()方法附着在 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()来处理取消/中断和资源的清理。它的运行条件是在 resolve 以前让 promise 取消。若是你取消的过晚,你就错过了取消的时机。

你可能比较好奇noop函数的做用是啥,noop 一词表示空操做,意指啥都不作。要是不指定这个函数,V8 引擎会抛出警告:UnhandledPromiseRejectionWarning: Unhandled promise rejection,因此老是记得去处理 promise 的 rejection 是个好习惯,即便你的句柄为 noop。

抽象的 promise 取消/中断

上面的wait()计时器固然是极好的,但咱们要继续将上面这种思路作进一步的抽象,来封装全部你须要知道的东西:

  1. 默认 reject 须要中断的 promise
  2. 记得要清理被 reject 过的 promise
  3. 保持警戒,onCancel的清理操做自己也有可能抛异常,该异常也须要处理。

让咱们来建立一个可中断的 promise 工具函数吧,这样你就能够用来包裹任何 promise 了。形式以下:

speculation(fn: SpecFunction, shouldCancel: Promise) => Promise
复制代码

SpecFunction就像你传入 Promise 构造器中的函数同样,惟一的不一样在于它有一个onCancel句柄:

SpecFunction(resolve: Function, reject: Function, onCancel: Function) => Void
复制代码
const speculation = (
  fn,
  cancel = Promise.reject() 
) => new Promise((resolve, reject) => {
  const noop = () => {};

  const onCancel = (
    handleCancel
  ) => cancel.then(
      handleCancel,
      noop
    )
    .catch(e => reject(e))
  ;

  fn(resolve, reject, onCancel);
});
复制代码

上例只是其做用要旨,其实还有须要边界状况须要你去考虑。我本身写了一个完整的版本供你们参考,speculation

结语

文章太长,翻译到后半段着实翻译不下去了,主要仍是自身对 promise 的理解还不够深,后面就看不懂了,但仍是以为要善始善终,把这件事作完,后面懂了再回头完善,never giveup!

参考文献

[1] Barbara Liskov; Liuba Shrira (1988). “Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems”. Proceedings of the SIGPLAN ’88 Conference on Programming Language Design and Implementation; Atlanta, Georgia, United States, pp. 260–267. ISBN 0–89791–269–1, published by ACM. Also published in ACM SIGPLAN Notices, Volume 23, Issue 7, July 1988.

相关文章
相关标签/搜索