四、Promise 链式流
4.1 链式流程控制
Promise
并不只是一个单步执行操作的机制,我们可以把多个 Promise
连接到一起以表示一系列异步步骤。
这种方式可以实现的关键在于以下两个 Promise
固有行为特性:
- 每次对
Promise
调用then(..)
,它都会创建并返回一个新的Promise
,我们可以将其链接起来; - 不管从
then(..)
调用的完成回调返回值是什么,它都会被自动设置为被链接的Promise
(第一点中的)的完成回调返回值。
先看一个示例:
let p = Promise.resolve(24)
p
.then(res => {
console.log(res) // 24
// 用值 48 完成连接的 promise
return res * 2
})
.then(res => {
// 这是链接的 promise
console.log(res) // 48
})
现在第一个 then(..)
就是异步序列中的第一步,第二个 then(..)
就是第二步。这可以一直任意扩展下去。
这里还需要考虑的一点是:如果需要步骤 2 等待步骤 1 异步来完成一些事情怎么办?我们使用了立即返回 return
语句,这会立即完成链接的 promise
。
使 Promise
序列真正能够在每一步有异步能力的关键是,回忆一下当传递给 Promise.resolve(..)
的是一个 Promise
或 thenable
而不是最终值时的运作方式。Promise.resolve(..)
会直接返回接收到的真正 Promise
,或展开接收到的 thenable
值,并在持续展开 thenable
的同时递归地前进。
从完成(或拒绝)处理函数返回 thenable
或者 Promise
的时候也会发生同样的展开。
let p = Promise.resolve(24)
p
.then(res => {
console.log(v) // 24
// 创建一个 promise 并返回
return new Promise((resolve, reject) => {
resolve(v * 2)
})
})
.then(res => {
console.log(res) // 48
})
虽然我们把 48 封装到了返回的 promise
中,但它仍然会被展开并最终成为链接的 promise
的决议,因此第二个 then(..)
得到的仍然是 48。
如果我们向封装的 promise
引入异步,一切都仍然会同样工作:
let p = Promise.resolve(24)
p
.then(res => {
console.log(v) // 24
// 创建一个 promise 并返回
return new Promise((resolve, reject) => {
// 引入异步
setTimeout(() => {
resolve(v * 2)
}, 500)
})
})
.then(res => {
// 在前一步中的 500ms 延迟之后运行
console.log(res) // 48
})
在这些例子中,一步步传递的值是可选的。如果不显式返回一个值,就会隐式返回 undefined
,并且这些 promise
仍然会以同样的方式链接在一起。这样,每个 Promise
的决议就成了继续下一个步骤的信号。
来看一个更实际的场景:
// 定义一个工具 request(..),用来构造一个表示 ajax(..) 调用完成的 promise
function request(url) {
return new Promise((resolve, reject) => {
// ajax(..) 回调应该是我们这个 promise 的 resolve(..) 函数
ajax(url, resolve)
})
}
request('http://some.url')
.then(res1 => {
return request(`http://some.url?v=${res1}`)
})
.then(res2 => {
console.log(res2)
})
我们构建的这个 Promise
链不仅是一个表达多步异步序列的流程控制,还是一个从一个步骤到下一个步骤传递消息的消息通道。
4.2 错误处理
如果这个 Promise
链中的某个步骤出错了怎么办?错误和异常是基于每个 Promise
的,这意味着可能在链的任意位置捕捉到这样的错误,而这个捕捉动作在某种程度上就相当于在这一位置将整条链“重置”回了正常运作:
// 步骤1:
request('http://some.url')
// 步骤2:
.then(res1 => {
foo.bar() // foo 未定义,出错
// 不会执行
return request(`http://some.url?v=${res1}`)
})
// 步骤3:
.then(
function fulfilled(res2) {
// 不会执行
console.log(res2)
},
function rejected(err) {
// 来自 foo.bar() 的错误 TypeError
console.log(err)
return 24
}
)
// 步骤4:
.then(res3 => {
console.log(res3) // 24
})
调用 then(..)
时的完成处理函数或拒绝处理函数如果抛出异常,都会导致(链中的)下一个 promise
因这个异常而立即被拒绝。
如果调用 promise
的 then(..)
,并且只传入一个完成处理函数,一个默认拒绝处理函数就会顶替上来:
let p = new Promise((resolve, reject) => {
reject('err')
})
p.then(
function fulfilled(res) {
// 不会执行
},
// 默认的拒绝处理函数,如果省略或传入任何非函数值
function(err) {
throw err
}
)
默认的拒绝处理函数只是把错误重新抛出,这最终会使得下一个 promise
的处理函数会用同样的错误理由拒绝。从本质上说,这使得错误可以继续沿着 Promise
链传播下去,直到遇到显式定义的拒绝处理函数。
小结
- 调用
Promise
的then(..)
会自动创建一个新的Promise
从调用返回。 - 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)
Promise
就相应地决议。 - 如果完成或拒绝处理函数返回一个
Promise
,它将会被展开,这样一来,不管它的决议值是什么,都会成为当前then(..)
返回的链接Promise
的决议值。
参考资料
- 《你不知道的JavaScript(中卷)》