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
如果 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
通过 then
和 catch
注册处理方法,在 Promise 解决或拒绝时调用。
promise
.then(value => { /* fulfillment */ })
.catch(error => { /* rejection */ })
使用回调的一个常见问题时,还来不及注册回调函数,异步执行就结束了。但 Promise 没有这个问题。如果一个 Promise 对象落定了,会保持住状态以及解决的值或拒绝的异常。此时再使用 then
或 catch
注册处理方法仍可以得到结果。
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、如果 onFulfilled
或 onRejected
返回一个 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、如果 onFulfilled
或 onRejected
返回了一个值,值传给 P。
该机制的主要作用是展平嵌套的 then()
调用,例子:
asyncFunc1()
.then(function (value1) {
asyncFunc2()
.then(function (value2) {
···
})
})
展平了的版本:
asyncFunc1()
.then(function (value1) {
return asyncFunc2()
})
.then(function (value2) {
···
})
3、如果 onFulfilled
或 onRejected
抛出了异常,则 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。