JavaScript Promise - 链式调用 (2/6)
到目前为止,promise 可能看起来只不过是使用回调和 setTimeout()
函数的某种组合的一种增量改进,但是 promise 远比我们看到的要复杂得多。更具体地说,有许多方法可以将
promise 链接在一起,以完成更复杂的异步行为。
链中的 catch()
Promise 的链式调用允许我们捕获前面的 promise 中出现的错误:
const promise = new Promise((resolve, reject) => {
throw new Error('Uh, oh!')
})
promise.catch(reason => {
console.log(reason.message) // Uh, oh!
throw new Error('Oops!')
}).catch(reason => console.error(reason.message)) // Oops!
在每个 promise 链末尾使用 catch()
方法捕获链中可能出现的错误是一个非常良好且必要的做法。
链中的 finally()
finally()
方法和 then()
,catch()
的不同之处在于,它将前一个 promise 的状态和值的拷贝作为自己的返回值,无论前一个 promise 成功还是失败:
const resolved = Promise.resolve(2)
resolved.finally(() => console.log('Resoleved Promise')).then(v => console.log(v)) // 2
const rejected = Promise.reject(3)
rejected.finally(() => console.log('Rejected Promise')).catch(v => console.error(v)) // 3
注意,finally()
返回的 promise 和前一个 promise 的状态和值相同,但并不是同一个 promise:
const p1= Promise.resolve(2)
const p2 = p1.finally(() => {})
console.log(p1 === p2) // false
但如果 finally()
中显式抛出错误或者返回一个失败的 promise,那么前一个 promise 就会被抛弃,取而代之的是这个错误或失败的 promise 被返回:
const p1 = Promise.reject(2)
p1.finally(() => {
throw 3
}).catch((v) => console.error(v)) // 3
p1.finally(() => Promise.reject(4)).catch((v) => console.error(v)) // 4
我们可以利用 finally()
的这个特点来做一些清理工作,同时保证链式调用中的错误总是被捕获:
const appElement = document.getElementById('app')
const promise = fetch('books.json')
appElement.classList.add('loading')
promise.then(response => {
if (response.ok) {
console.log(response.status)
} else {
throw new Error(`Unexpected status code: ${response.status} ${response.statusText}`)
}
}).finally(() => {
appElement.classList.remove('loading')
}).catch(reason => {
console.error(reason.message)
})
上面的代码保证了 loading
类被移除,同时 then()
方法中若抛出错误也会被 catch()
捕获,这也是为什么 catch()
方法总是应该放到链式调用的末尾。
从链中返回值
Promise 链式调用的另一个强大的能力是可以将值传递到下一个 promise。
// resolve
const p1 = Promise.resolve(2)
p1.then(v => v+1).then(v => console.log(v)) // 3
// reject
const p2 = Promise.reject(2)
p2.catch(v => {
return v + 2
}).then(v => console.log(v)) // 4
其实这个值是被包装成 promise 返回的:
const p1 = Promise.resolve(2)
const p2 = p1.then(v => v+2)
console.log(p2 instanceof Promise) // true
// p2 等价于
const p3 = p1.then(v => Promise.resolve(v+2))
但 finally()
有点不同,上面说过,finally()
将前一个 promise 的状态和值拷贝到一个新的 promise,然后传递给下一个 promise,所以 finally()
中的返回值会被忽略:
const p1 = Promise.resolve(2)
p1.finally(v => v+1).then(v => console.log(v)) // 2
const p2 = Promise.reject(2)
p2.finally(v => {
return v + 2
}).catch(v => console.error(v)) // 2
从链中返回 promise
从 promise 中返回原语/值使得数据可以在 promise 链中传递,如果我们从 promise 中返回的是一个 promise 对象,那么下一步如何操作将取决于这个 promise 的状态:
const p1 = Promise.resolve(2)
const p2 = Promise.reject(3)
p1.then(v => {
console.log(v)
return p2
}).then(v => console.log(`resolved: ${v}`))
.catch(v => console.error(`rejected: ${v}`))
/*
2
rejected: 3
*/
由于 p2
是一个失败的 promise,因此上面的代码会跳过第二个 then()
方法,转而执行 catch()
方法。 还有一点值得注意的是,上面 catch()
方法的 rejection handler
并不是添加到 p2
promise 上的,而是一个新的 promise,上面的代码等价于:
const p1 = Promise.resolve(2)
const p2 = Promise.reject(3)
const p3 = p1.then(v => {
console.log(v)
return p2
})
p3.then(v => console.log(`resolved: ${v}`))
.catch(v => console.error(`rejected: ${v}`))
当一个操作需要不止一个 promise 才能完成时,这种从 promise 中返回 promise 的能力可以极大地简化我们的代码、降低心智负担。对比一下下面使用这种能力与否的代码差异:
const p1 = fetch('books.json')
// 不利用从 promise 返回 promise 的能力
p1.then(response => {
const p2 = response.json()
p2.then(payload => console.log(payload))
.catch(reason => console.error(reason.message))
}).catch(reason => console.error(reason.message))
// 利用从 promise 返回 promise 的能力
p1.then(response => response.json())
.then(payload => console.log(payload))
.catch(reason => console.error(reason.message))
从 promise 返回 promise 不会影响 promise 的 executor 的执行顺序,promise 的 executor 仍然按照 promise 定义的顺序执行:
const p1 = Promise.resolve(2)
p1.then(v => {
console.log(v)
const p2 = new Promise((resolve, reject) => {
console.log(101)
setTimeout(() => {
console.log(103)
resolve(3)
console.log(104)
}, 2000)
console.log(102)
})
console.log(100)
return p2
}).then(v => console.log(v))
/*
2
101
102
100
103
104
3
*/
使用 setTimeout()
将 p2
的 executor 延迟 2000 毫秒,这更像是模拟我们在实践中可能发出的网络或文件处理请求。当我们希望在开始新的异步操作之前等待之前的 promise 已经解决时,这个模式将非常有用。
总结
- 最佳实践是保证每个 promise 调用链的末尾都有一个
catch()
方法来捕获链中可能出现的错误 - 链式调用中
finally()
方法总是把前一个 promise 的拷贝作为自己的返回值 - 但如果
finally()
自身发生错误或者返回的是一个失败的 promise,会丢弃前一个 promise 的拷贝,返回自身的返回值 - 每一次对
then()
,catch()
,finally()
方法的调用都是在创建并返回一个新的 promise- 如果返回值是一个原语,则会被包装成一个 resolved 的 promise
- 如果返回值是一个 promise,则返回这个 promise 的副本
finally()
依然是遵守 2 和 3