ES 流程控制进阶 —— Promises

published

Promises 的历史

Promises 曾经以多种形式存在于很多语言中。这个词最先由 C++ 工程师用在 Xanadu 项目 —— Web 应用项目的先驱。随后 Promises 被用在 E 语言中,这又激发了 Python 开发人员的灵感,将它实现成了 Twisted 框架的 Deferred 对象。 2007 年,Promises 赶上了 JS 大潮,那时 Dojo 框架刚从 Twisted 框架汲取灵感,新增了一个叫做 dojo.Deferred 的对象。也就在那个时候,相对成熟的 Dojo 框架与初出茅庐的 jQuery 框架激烈地争夺着人气和名望。2009 年,Kris Zyp 有感于 dojo.Deferred 的影响力提出了 CommonJS 的 Promises/A 规范。

同年,Node.js 首次亮相。

Promises 的标准

  • A
  • A+
  • B
  • KISS
  • C
  • D

阅读 http://wiki.commonjs.org/wiki/Promises 了解更多。

Promises 的同义词

  • Future
  • Defer

Promises 的开源实现

  • Q
  • BlueBird

ES2015 已经开始支持 Promises。

为什么引入 Promises?

引入 Promises 必定是用来解决某种问题。首先来看看编码中所面临的问题。

下面的同步 ES 函数用来读取文件内容,并将其内容解析为 JSON。这个函数简单易读,但是你绝对不想把它放到有高并发需求的项目里去,因为这个函数是阻塞的 —— 当你从磁盘读取文件内容时,事件循环被阻塞了,它除了等,什么都干不了。

function readJSONSync(filename) {
  return JSON.parse(fs.readFileSync(filename, 'utf8'))
}

为了让应用性能更高,需要让所有包含 IO 的操作都变成异步的。最简单的方法就是使用回调函数:

function readJSON(filename, callback) {
  fs.readFile(filename, 'utf8', function (err, res) {
    if (err) return callback(err)

    callback(null, JSON.parse(res))
  })
}

但是,上面的代码有诸多问题:

  • callback 参数模糊了输入与输出
  • 没有对 JSON.parse() 可能抛出的错误进行处理

callback 参数的问题是没法去掉的,因为它是异步回调中必需的参数。那就处理 JSON.parse() 可能抛出的错误吧(注意:不要处理回调函数抛出的错误):

function readJSON(filename, callback) {
  fs.readFile(filename, 'utf8', function (err, res) {
    if (err) return callback(err)

    try {
      res = JSON.parse(res)
    } catch (e) {
      return callback(e)
    }

    callback(null, res)
  })
}

处理好了,但是错误处理的过程繁琐不直观。 看起来,通过回调函数实现异步的方式有些缺点:

  • 错误处理繁琐不直观:
    • 两种处理错误的方式:异步函数的错误优先错误处理 / try-catch 的错误处理
    • 当同步代码位于异步函数的回调函数中时,需要小心翼翼地组合这两种错误处理的方式
  • 回调函数混淆了输入与输出:
    • 在同步函数中,函数的参数就是输入,函数返回的内容就是输出
    • 在异步函数中,回调函数既是输入,也是输出

为了解决上述问题,引入 Promises。

The point of promises is to give us back functional composition and error bubbling in the async world. Promises are about making asynchronous code retain most of the lost properties of synchronous code such as flat indentation and one exception channel. —— Petka Antonov

Promises 让 ES 中的异步编程重新获得同步编程的特性:

  • 可传递的异常 —— 可以使用单个异常捕获对所有异常进行处理
  • 单向的数据流,并且异步操作有了返回值(虽然是一个来自未来的值) —— 可以像同步编程一样进行函数组合

什么是 Promise?

下文使用 Promise 来指代 Promise 对象。

Promise 的定义

Promise 是异步编程的一种抽象。一个 Promise 对象代理一个「还未获取到的值」。通过这个对象,为这个值注册回调函数。获取到值后,执行对应的回调函数。

比如,当一个异步函数被调用时,函数立即返回一个 Promise,然后就可以使用这个 Promise,为将要得到的异步函数的执行结果注册回调函数。获取到执行结果后,执行回调函数。

Promise 的三种状态

Promise 有三种状态:

  • pending - Promise 还没有获取到值
  • fulfilled - Promise 成功地获取到了值
  • rejected - Promise 没有获取到值。这时,会提供给 Promise 一个没有获取到值的原因,通常是一个 Error 对象
  • settled - 为 fulfilled 与 rejected 的统称 Promise 的两种 settled 状态对应两种值:
    • fulfilled:fulfillment value
    • rejected: rejected reason

Promise 状态间的转换

转换 Promise 状态的两种途径:

  • fulfill:状态由 pending 变为 fulfilled;
  • reject:状态由 pending 变为 rejected Promise 的状态转换是单向且单次的;

转换发生后,任何状态转换的尝试都会被忽略。

resolve / resolved

resolve(解决) 表示「解决如何确定 Promise 的状态的问题 —— 如何将 Promise 的状态设置为 fulfilled 或 rejected」:

  • 使用一个普通值来 resolve 一个 Promise,则转换 Promise 的状态为 fulfilled
  • 使用一个 Promise[2] 来 resolve 一个 Promise[1],则前者[1]继承后者[2]的状态

resolved(已经解决了) 表示「已经知道如何确定 Promise 的状态,但状态不一定已经确定了」:

  • 状态已经 settled(状态已经确定)
  • 状态已经和另外一个 Promise 的状态相关联(已经知道如何确定状态,但还没确定)

词汇回顾

  • pending
  • fulfilled
  • rejected
  • settled
  • fulfilled value
  • rejection reason
  • resolve
  • resolved

创建 Promise

new Promise()

new Promise(function (resolve, reject) {})

function (resolve, reject) {} 被称为 resolver。

除此之外, Promise API 还提供了另外两个方法,用来更方便地创建初始状态为 resolved 或 rejected 的 Promise。

为什么不是 fulfilled,而是 resolved?上面的解释 resolved 地方提到,resolve() 另一个 Promise 时,当前 Promise 的状态是还未决定的。所以不能使用 fulfilled。

Promise.resolve()

Promise.resolve('the short way')

new Promise(function (resolve, reject) {
  resolve('the long way')
})

Promise.reject()

Promise.reject('the short rejection')

new Promise(function (resolve, reject) {
  reject('the long rejection')
})

使用 Promise

常规调用

使用 then()/catch() 为 Promise 注册回调函数。使用 then()/catch() 进行注册的函数称为 Promise 的 reaction。

promise
  .then(value => { /* fullfillment */ })
  .catch(error => { /* rejection */ })

then() 方法返回一个新的 Promise。(这是链式调用 Promise 的基础)

const p1 = Promise.resolve()
const p2 = p1.then(function () {})

console.log(p1 !== p2)  // true

这里有一点需要说明,如果传给 then() 的方法并非函数,那么 then() 的调用会被解释为 then(null),这会导致前一个 Promise 的结果穿过当前的 then(),直接传入 then() 的下一级,如:

Promise.resolve('hello').then(Promise.resolve('world')).then(function (result) {
  console.log(result)  // hello
});

catch(reaction) 只是 then(null, reaction) 的一个语法糖,下面的两种调用方法是等效的:

promise.catch((err) => {
  /* rejection */
})

promise.then(null, (err) => {
  /* rejection */
})

链式调用

上面提到,每次调用 then() 都会返回一个新的 Promise。新的 Promise 使用 then()的参数的返回值来 resolve 自身。

Promise.resolve('ta-da!').then(
  function step2 (result) {
    console.log('step2 recieved: ' + result)
    return 'Greeting from step2'
  }
).then(                         // Promise.resolve('Greeting from step2')
  function step3 (result) {
    console.log(result)
  }
).then(
  function step4 (result) {     // Promise.resolve(undefined)
    console.log('step4 recieved: ' + result)
    return Promise.resolve('fulfilled value')
  }
).then(                         // Promise.resolve(Promise.resolve('fulfilled value'))
  function step5 (result) {
    console.log('step5 received: ' + result)
  }
).catch(
  function errHandler (err) {
    console.log(err)
  }
)

示例

延时函数

function delay (ms) {
  return new Promise(function (resolve, reject) {
    setTimeout(resolve, ms)
  })
}

delay(3000).then(function () {
  console.log('3 seconds have passed')
})

错误处理

上面提到,回调形式的异步 API 在处理异常的时候显得特别繁琐不直观:

fs.readFile(__filename, (err, data) => {
  if (err) {
    return console.log(err)
  } else {
  }
})

好在,Promise 可以解决这个问题 —— 使用一个 catch() 方法替换掉这些重复的错误检查。

Rejection Reason 的处理

Promise 状态转换为 rejected 的情况

  • reject() 被调用
  • Promise 执行的回调函数(传给 Promise 构造函数、then()catch() 的回调函数)中抛出错误
const rejectedPromise = new Promise(function (resolve, reject) {
  reject(new Error('Ops!'))
})

rejectedPromise.catch(function (err) {
  console.log('Rejected')
  console.log(err)
})

const rejectedPromise = new Promise(function (resolve, reject) {
  throw new Error('Ops!')
})

rejectedPromise.catch(function (err) {
  console.log('Rejected')
  console.log(err)
})

Rejection Reason 的传递

Rejection Reason 顺着 Promise 链式结构传递。 当一个 Promise 状态为 rejected 后,Rejection Reason 会顺着链式结构传递,直到遇到一个 catch() 为止。 在实践中,catch() 被用在链式结构的末尾以捕获所有 Rejection Reason。

Promise.reject(Error('bad news')).then(
  function step2 () {
    console.log('This is never run')
  }
).then(
  function step3 () {
    console.log('This is also never run')
  }
).catch(
  function (error) {
    console.log('Something failed along the way. Inspect error for more info')
    console.error(error)
  }
)

Promise API

Promise 构造函数

new Promise(function (resolve, reject) { ... }) returns promise

使用 new 调用 Promise 构造函数会创建一个 Promise。Promise 使用 then()/catch() 注册回调函数。当 Promise 的状态转换为 fulfilled 或 rejected 时,回调函数被执行。

promise.then

promise.then([onFulfilled], [onRejected]) returns promise

promise.then() 方法接收两个可选参数 onFulfilledonRejected。返回一个使用 onFulfilledonRejected 的返回值进行 resolve 的 Promise。

promise.catch

promise.catch(onRejected) returns promise

promise.catch(onRejected)promise.then(null, onRejected) 的语法糖。返回一个使用 onRejected 返回值或 onRejected 中抛出的错误进行 resolve 或 reject 的 Promise。

Promise.resolve

Promise.resolve([value|promise]) returns promise

Promise.resolve() 是一个创建「使用给定值 resolve 」的 Promise 的快捷方法。

Promise.reject

Promise.reject([reason]) returns promise

Promise.reject() 是一个创建「使用给定原因 reject」的 Promise 的快捷方法。

Promise.all

Promise.all(iterable) returns promise

Promise.all() 接收一个可迭代对象,如 Array, Set,自定义可迭代对象。返回一个使用 「包含可迭代对象中对象执行结果的 Array」进行 resolve 的 Promise。 返回的 Promise :

  • 在可迭代对象中的所有 Promise 状态都为 fulfilled 时,其状态才转换为 fulfilled
  • 在可迭代对象中的任意一个 Promise 状态为 rejected 时,其状态才转换为 rejected

Promise.race

Promise.race(iterable) returns promise

Promise.race() 接收一个可迭代对象作为参数,检查可迭代对象中的每一个项,直到找到某一项不是 Promise 或者 Promise 已经 settled。然后,返回「使用找到的这一项进行 resolve 」的 Promise。 如果可迭代对象中只包含未 settled 的 Promise,那么返回「使用第一个 settled Promise 进行 resolve」的 Promise。

封装 Callback API 为 Promise API

要想让 Callback API 变为 Promise API,需要对其进行封装。 以封装 fs.readFile 为例。

参数处理:除 Callback 外,其他参数都放到新的函数的参数中

function readFile (file) {
}

返回值处理:返回 Promise 对象

function readFile (file) {
  return new Promise(function (resolve, reject) {
  })
}

结果处理:通过 resolve 和 reject 重塑流程

function readFile (file) {
  return new Promise(function (resolve, reject) {
    fs.readFile(file, (err, data) => {
      if (err) return reject(err)

      resolve(data.toString())
    })
  })
}

通常,并不推荐自行将 Node.js 回调风格的 API 封装为 Promise,因为在处理琐碎的细节时,很容易出现疏漏。更推荐使用 bluebird 提供的 Promise.promisify(或类似工具库) 来帮助我们完成转换。

Promise 的流程可预测性

  • resolver 的执行是同步的
  • 通过 then()/catch() 注册的 reaction 的执行是异步的:
    • 在 Promise settled 之前注册的 reaction,会等待 Promise settled 后才开始使用计算结果
    • 在 Promise settled 之后注册的 reaction,会使用 Promise settled 时缓存的计算结果

Promises/A+ 标准中针对 then() 方法,这么写到

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

这表示,你的代码可以依赖 run-to-completion 语义,并且链式的 Promise 不会阻塞 Event Loop:

const promise = new Promise(function (resolve, reject) {
  console.log('Inside the resolver function')
  resolve()
})

promise.then(function () {
  console.log('Inside the onfulfilled handler')
})

console.log('This is the last line of script')

/**
 * Output:
 * Inside the resolver function <- resolver 的执行是同步的
 * This is the last line of script
 * Inside the onfulfilled handler  <- reaction 的执行是异步的
 */

另外,这种限制可以避免开发者写出返回方式不统一的函数 —— 有时同步返回,有时异步返回(这是一种反模式,因为它让代码不可预测,更多信息请阅读 Designing APIs for Asynchrony)。

反模式

拓展阅读

还有更多进阶内容,请阅读 [翻译] We have a problem with promises。文中介绍了更多直接编码过程中会遇到的问题,值得阅读。

书籍推荐

其他资源