Promise详解

Promise 是一种处理异步的思路。Promise 也是一个类。当我们说一个 promise 的时候,一般指一个 Promise 对象。
快速感受 传统使用回调的写法:
fs.readFile('config.json', function (error, text) { if (error) { console.error('Error while reading config file') } else { try { const obj = JSON.parse(text) console.log(JSON.stringify(obj, null, 4)) } catch (e) { console.error('Invalid JSON in file') } } })

使用 Promise 的写法:
function readFilePromisified(filename) { return new Promise(function (resolve, reject) { fs.readFile(filename, { encoding: 'utf8' }, (error, data) => { if (error) { reject(error) } else { resolve(data) } }) }) }readFilePromisified('config.json').then(function (text) { // (A) const obj = JSON.parse(text) console.log(JSON.stringify(obj, null, 4)) }).catch(function (error) { // (B) // File read error or JSON SyntaxError console.error('An error occurred', error) })

其中 readFilePromisified 方法可以通过库快速改写,如通过 bluebird 库:
const readFilePromisified = bluebird.promisify(fs. fs.readFile)

优点 这里先提几个关键点,具体优点还需要边学边做体会。
使用 Promise 代替回调等机制,处于两类目的。一类是 Promise 比相应写法更好,简练方便,清晰,表达力强等等。第二类是相应机制的一些功能,如重复回调、错误处理、回调注册时机、组合多个异步操作等,非常容易出问题或非常难写,而 Promise 可以规避这类问题。
最后一点是 Promise 统一了标准化的写法。回调并没有统一的标准,Node.js 的回调,XMLHttpRequest 的回调并不统一。但 Promise 是统一的。
状态 一个 Promise 会处于下面三种状态中的一种:
  • 异步结果就绪前,Promise 处于 pending 状态
  • 获得结果后,Promise 处于 fulfilled 状态
  • 如果发生了错误,Promise 处于 rejected 状态。
Promise 落定(settled),指它已获得了结果或发生了错误(fulfilled 或 rejected)。Promise 一旦落定,状态不会再改变。
Promise 提供两种接口使 Promise 从进行中的状态(pending)达到落定状态(settled):
  • 一类是“拒绝”(reject)方法,让 Promise 进入 rejected 状态。
  • 一类是“解决”(resolve)方法。如果解决的值不是一个 Promise,则进入 fulfilled 状态;否则如果解决的值本身又是一个 Promise,则要等这个 Promise 达到落定状态。
创建 Promise 通过构造器
const p = new Promise(function (resolve, reject) { // (A) ··· if (···) { resolve(value) // success } else { reject(reason) // failure } })

并且如果在函数中抛出了异常,p 自动被该异常拒绝。
const p = new Promise(function (resolve, reject) { throw new Error("Bad") }) p.catch(function(e) { // 将执行这里 console.error(e) })

thenable
thenable 是一个对象,具有类似与 Promise 的 then() 方法。thenable 不是一个 Promise。它一般是 Promise 标准化之前出现的采用 Promise 思想的自定义的对象。通过 Promise.resolve(x) 可以将一个非标准化的 thenable 转换为一个 Promise,见下一节。
Promise.resolve(x)
对于 x 不是一个 Promise,Promise.resolve(x) 返回一个 Promise,该 Promise 立即以 x 的值解决。
Promise.resolve('abc') .then(x => console.log(x)) // abc

如果 x 是一个 Promise,则 Promise.resolve(x) 原样返回 x
const p = new Promise(() => null) console.log(Promise.resolve(p) === p) // true

【Promise详解】如果 x 是一个 thenable,则把它转换为一个 Promise。
const fulfilledThenable = { then(reaction) { reaction('hello') } } const promise = Promise.resolve(fulfilledThenable) console.log(promise instanceof Promise) // true promise.then(x => console.log(x)) // hello

总结:通过 Promise.resolve() 将任何值转换为一个 Promise。
Promise.reject(err)
Promise.reject(err) 返回一个 Promise,以 err 拒绝:
const myError = new Error('Problem!') Promise.reject(myError) .catch(err => console.log(err === myError)) // true

例子
1、fs.readFile()
import {readFile} from 'fs'function readFilePromisified(filename) { return new Promise(function (resolve, reject) { readFile(filename, { encoding: 'utf8' }, (error, data) => { if (error) { reject(error) } else { resolve(data) } }) }) }

2、XMLHttpRequest
function httpGet(url) { return new Promise(function (resolve, reject) { const request = new XMLHttpRequest() request.onload = function () { if (this.status === 200) { resolve(this.response) } else { reject(new Error(this.statusText)) } } request.onerror = function () { reject(new Error('XMLHttpRequest Error: '+this.statusText)) } request.open('GET', url) request.send() }) }

3、延时 Let’s implement setTimeout() as the Promise-based function delay() (similar to Q.delay()).
function delay(ms) { return new Promise(function (resolve, reject) { setTimeout(resolve, ms) }) }// Using delay(): delay(5000).then(function () { console.log('5 seconds have passed!') })

4、超时 如果一个 Promise 在指定时间内获取到了结果(落定),则通知这个结果,否则以超时异常拒绝:
function timeout(ms, promise) { return new Promise(function (resolve, reject) { promise.then(resolve) setTimeout(function () { reject(new Error('Timeout after '+ms+' ms')) }, ms) }) }

消费一个 Promise 通过 thencatch 注册处理方法,在 Promise 解决或拒绝时调用。
promise .then(value => { /* fulfillment */ }) .catch(error => { /* rejection */ })

使用回调的一个常见问题时,还来不及注册回调函数,异步执行就结束了。但 Promise 没有这个问题。如果一个 Promise 对象落定了,会保持住状态以及解决的值或拒绝的异常。此时再使用 thencatch 注册处理方法仍可以得到结果。
then 还有一个两参数的写法:
promise.then( null, error => { /* rejection */ })promise.catch( error => { /* rejection */ })

传递 then() 方法会返回一个新的 Promise,于是你可以链接调用:
const q = Promise.resolve(true).then(function() { return new Promise(function(resolve) { setTimeout(function() { resolve("Good") }, 1000) }) }) q.then(function(result) { console.log(result) // Good })

上述代码其实先后会产生 4 个 Promise:
const q1 = Promise.resolve(true) const q2 = q1.then(function() { const q3 = new Promise(function(resolve) { setTimeout(function() { resolve("Good") }, 1000) }) return q3 }) const q4 = q2.then(function(result) { console.log(result) })

具体来说,.then(onFulfilled, onRejected) 返回的 Promise P 的值取决于它的回调函数的执行。
1、如果 onFulfilledonRejected 返回一个 Promise,该 Promise 的结论传递给 P。例子:
Promise.resolve(true) .then(function (value1) { return 123 }) .then(function (value2) { console.log(value2) // 123 })

特别注意如果 onRejected 中正常返回了值,则 then() 的结果是解决(fulfilled)而不是拒绝状态。可以利用这种特性恢复错误,提供默认值等。
Promise.reject("Bad") .catch(function () { return "I Know BAD" }) .then(function (result) { console.log(result) })

2、如果 onFulfilledonRejected 返回了一个值,值传给 P。
该机制的主要作用是展平嵌套的 then() 调用,例子:
asyncFunc1() .then(function (value1) { asyncFunc2() .then(function (value2) { ··· }) })

展平了的版本:
asyncFunc1() .then(function (value1) { return asyncFunc2() }) .then(function (value2) { ··· })

3、如果 onFulfilledonRejected 抛出了异常,则 P 以该异常拒绝。
asyncFunc() .then(function (value) { throw new Error() }) .catch(function (reason) { // Handle error here })

链式调用有一个好处,可以最后统一处理错误。中间环节的任何错误可以被最终统一处理:
asyncFunc1() .then(asyncFunc2) .then(asyncFunc3) .catch(function (reason) { // Something went wrong above })

串行与并行 通过 then 链式调用异步函数,这些函数的执行是串行的:
asyncFunc1() .then(() => asyncFunc2())

如果不使用 then 连接,它们是并行执行的,但是你拿不到结果,也不知道什么时候他们全部完成。
asyncFunc1() asyncFunc2()

解决方法是使用 Promise.all()。它的参数是一个数组,数组元素是 Promise。它返回一个 Promise,解析的结果是一个数组。
Promise.all([ asyncFunc1(), asyncFunc2() ]) .then(([result1, result2]) => { ··· }) .catch(err => { // Receives first rejection among the Promises ··· })

如果 map 的映射函数返回一个 Promise,map 产生的数组由 Promise.all() 处理:
const fileUrls = [ 'http://example.com/file1.txt', 'http://example.com/file2.txt', ] const promisedTexts = fileUrls.map(httpGet)Promise.all(promisedTexts) .then(texts => { for (const text of texts) { console.log(text) } }) .catch(reason => { // Receives first rejection among the Promises })

Promise.race()Promise.all() 类似,但只要数组中一个 Promise 落定(不管解决还是拒绝),该 Promise 的结果作为 Promise.race() 的结果。
注意如果数组为空,Promise.race() 永远不会落定(settled)。
例子,通过 Promise.race() 实现超时:
Promise.race([ httpGet('http://example.com/file.txt'), delay(5000).then(function () { throw new Error('Timed out') }) ]) .then(function (text) { ··· }) .catch(function (reason) { ··· })

常见错误 丢失 then 的结果
先看错误的代码:
function foo() { const promise = asyncFunc() promise.then(result => { ··· })return promise }

再看正确的代码:
function foo() { const promise = asyncFunc() return promise.then(result => { ··· }) }

甚至再简化为:
function foo() { return asyncFunc() .then(result => { ··· }) }

不要忘了 promise.then( 也会产生一个结果,甚至可能抛出异常,或返回一个异步结果(一个新的 Promise)。
捕获全部异常
前面提过可以通过链式调用,最后统一处理异常,这么做不但省事,而且可以避免遗漏错误:
asyncFunc1() .then( value => { // (A) doSomething() // (B) return asyncFunc2() // (C) }, error => { // (D) ··· })

上面的代码,D 处的错误处理只能处理 asyncFunc1() 的错误,但无法处理 B 处抛出的移除和 C 处返回的拒绝的 Promise。正确的写法:
asyncFunc1() .then(value => { doSomething() return asyncFunc2() }) .catch(error => { ··· })

忽视非异步代码的错误
有时我们编写的异步方法中 —— 这里的异步方法指返回 Promise 的方法 —— 在异步前存在部分非异步的代码。如果这些代码抛出异常,异常将直接从方法抛出,而不是进入返回 Promise 并拒绝,例如下面代码的 A 处:
function asyncFunc() { doSomethingSync() // (A) return doSomethingAsync() .then(result => { ··· }) }

解决方法一,捕获并显式拒绝:
function asyncFunc() { try { doSomethingSync() return doSomethingAsync() .then(result => { ··· }) } catch (err) { return Promise.reject(err) } }

解决方法二,通过 Promise.resolve().then() 开始一段 Promise 链,将同步代码包裹进 then 的方法中:
function asyncFunc() { return Promise.resolve().then(() => { doSomethingSync() return doSomethingAsync() }).then(result => { ··· }) }

方法三:
function asyncFunc() { return new Promise((resolve, reject) => { doSomethingSync() resolve(doSomethingAsync()) }) .then(result => { ··· }) }

This approach saves you a tick (the synchronous code is executed right away), but it makes your code less regular.
finally 有时不管成功还是失败都要执行一些方法,例如确认对话框,不管确定还是取消都要关闭对话框。Promise 原生 API 没有提供 finally 的功能,但我们可以模拟:
Promise.prototype.finally = function (callback) { const P = this.constructor // We don’t invoke the callback in here, // because we want then() to handle its exceptions return this.then( // Callback fulfills => continue with receiver’s fulfillment or rejection // Callback rejects => pass on that rejection (then() has no 2nd parameter!) value=> P.resolve(callback()).then(() => value), reason => P.resolve(callback()).then(() => { throw reason }) ) }

调用:
createResource(···) .then(function (value1) { // Use resource }) .then(function (value2) { // Use resource }) .finally(function () { // Clean up })

Promise 库 原生 Promise 库够用但不够强大,对于更复杂的功能,可以使用某个第三方 Promise 库。比如 bluebird。

    推荐阅读