logo

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 已经解决时,这个模式将非常有用。

总结

  1. 最佳实践是保证每个 promise 调用链的末尾都有一个 catch() 方法来捕获链中可能出现的错误
  2. 链式调用中 finally() 方法总是把前一个 promise 的拷贝作为自己的返回值
  3. 但如果 finally() 自身发生错误或者返回的是一个失败的 promise,会丢弃前一个 promise 的拷贝,返回自身的返回值
  4. 每一次对 then()catch()finally() 方法的调用都是在创建并返回一个新的 promise
    • 如果返回值是一个原语,则会被包装成一个 resolved 的 promise
    • 如果返回值是一个 promise,则返回这个 promise 的副本
    • finally() 依然是遵守 2 和 3