📝最佳实践

使用 Promise 编程时的最佳实践和注意事项

1. 最佳实践

A good rule of thumb is to always either return or terminate promise chains, and as soon as you get a new promise, return it immediately, to flatten things.

1.1 总 return

在写 then() 的回调函数时,记得 return 结果,否则下一个 then() 的回调是拿不到前面的处理结果的,也就是说 promise chain 断了。

尤其是当 onFulfilled/onRejected 里定义了一个新 promise 时,更要将其返回,否则该 promise 就没地方跟踪它的 settlement 了。与此同时,下一个 then() 也会被提前调用,而且 fulfilled 的 valueundefined(即上面提到的“promise chain 断了”)。在这种情况下,我们就有了两条独立的 promise chain,其中,里面的 chain 没人管,外面的 chain 还断了。

doSomething()
    .then((url) => {
        // 没有 return
        fetch(url)  // 1. 它的执行结果到底是什么,没人知道
    })
    .then((result) => {
        console.log(result) // 2. undefined,因为上一个 handler 没返回任何值
    })

所以,在写 onFulfilled/onRejected 回调函数时,最好总是 return 结果。而且一旦遇到了 promise,务必将其 return,并 defer 到下一个 then 里再处理其结果。

1.2 总用 catch 终止

如果一个 promise chain 没有用 catch 终止,就有可能出现 uncaught errors

1.3 不随便嵌套 promise

promise chain 不要随便 nest(嵌套),尽量保持 flat。

nesting 是一种控制结构,用来限制 catch 语句的范围。正确使用 nest 可以提高 error 恢复的精度。比如:

doSomethingCritical()
    .then((result) =>
        doSomethingOptional(result)
            .then((optionalResult) => doSomethingExtraNice(optionalResult))
            .catch((e) => { })
    ) // Ignore if optional stuff fails. proceed.
    .then(() => moreCriticalStuff())
    .catch((e) => console.error(`Critical failure: ${e.message}`))

下面,对比看看 promise chain 的两种写法:

const listOfIngredients = []

// 1. nest
doSomething()
    .then((url) => fetch(url).then((res) => res.json()).then((data) => { listOfIngredients.push(data) }))
    .then(() => { console.log(listOfIngredients) })
    
// 2. flat(更推荐把 promise chain 展开)
//    一旦得到了一个新的 promise,就立即返回,以 flatten
doSomething()
    .then((url) => fetch(url))
    .then((res) => res.json())
    .then((data) => {
        listOfIngredients.push(data)
    })
    .then(() => {
        console.log(listOfIngredients)
    })

1.4 举例

看看下面的代码,有哪几处错误?

doSomething()
    .then((result) => {
        doSomethingElse(result)
            .then((newResult) => doThirdThing(newResult))
    })
    .then(() => doFourthThing())
点击查看答案

有三处:

  1. 第 2 行的 promise 没有 return

    • not chain things together properly,通常发生在创建了一个新 promise 但忘了将其 return

    • 后果就是:the chain is broken 或者说有了两个独立的 chains racing

  2. 不必要的嵌套

  3. 没有用 catch 终止 chain

修正后的代码如下:

doSomething()
    .then((result) => doSomethingElse(result))
    .then((newResult) => doThirdThing(newResult))
    .then(() => doFourthThing())
    .catch(error => { console.log(error) })

这样,我们就有了一个带 error 处理的 deterministic(确定性的)chain。

2. 错误处理

2.1 catch()

catch() 会捕获 thrown 语句抛出的异常,以及代码的 errors。

对于相同的错误处理逻辑,可以只写一个 catch 语句。比如:

doSomething()
    .then((result) => doSomethingElse(result))
    .then((newResult) => doThirdThing(newResult))
    .then((finalResult) => console.log(`Got the final result: ${finalResult}`))
    .catch(failureCallback)
// 由于前面的三个 then() 都没有 onRejected 参数
// 所以会把 rejection reason 一路透传到第四个 catch()

2.2 全局 event

如果 promise 的 rejection 事件没有被任何 handler 处理,那么它就会冒泡到 call stack 的顶部,而 host(宿主)需要把 error 显示出来。

在 web 中,当 promise 被 rejected 时,以下两个 events 之一会被发送到 global scope,它们的发送时机分别是:

  • unhandledrejection(未处理的拒绝):当 promise 被 rejected 了但没有可用的 rejection handler 时

  • rejectionhandled(拒绝处理):当 handler 被附加到(已引起 unhandledrejection 事件的)rejected promise 上时

在 Node.js 中,是监听 unhandledRejection 事件。

// process.on() 可避免 error 输出到 console(控制台)
process.on('unhandledRejection', (reason, promise) => {
    // Add code here to examine the 'promise' and 'reason' values
})

2.3 reject 写法

以下语句都可以触发 promise 的 reject 的逻辑:

reject("oh, no!")
// reject(new Error("oh, no!"))

// throw ('oh, no!')
throw new Error('oh, no!')  // 注意:异步 throw,就和 uncaught error 一样

return Promise.reject('oh, no!')
// return Promise.reject(new Error('oh, no!'))

其中,被注释的语句是相对不推荐的。推荐的做法是:

  1. 正常返回用 reject()Promise.reject()。具体选哪个,取决于返回值的类型:

    • 若返回值的类型是非 promise,则用前者,以便直接进入 rejected 状态的逻辑

    • 若要返回 promise 类型,则用后者,毕竟它内部新建一个 promise 还是要花内存和时间的

  2. 出错用 throw new Error() 显式抛出错误,好处是:

    • 有语义,表示出错了

    • 能保留 stack trace(堆栈跟踪)等有价值的信息

    • 可以避免 catch 部分做类型检查——判断是 String 还是 Error

    • 性能方面的小顾虑:有一种说法是 throw 用多了会影响性能,因为代码有可能跳到不可预知的位置。但是结合 Promise 的实现,我们知道回调函数的执行都被包在了 try-catch 块里,所以代码的跳跃幅度是在可控范围之内的。因此,这里的性能损失可以忽略。

3. 组合工具

composition tools

3.1 同时执行

“同时”运行多个 promises,不相互阻塞。

  • Promise.race()

  • Promise.all()

  • Promise.allSettled()

  • Promise.any()

3.2 顺序执行

顺序执行多个 promises,比如:

// eg1.
Promise.resolve()
    .then(func1)
    .then(func2)
    .then(func3)
    .then((result3) => {
        /* use result3 */
    })


// eg2. 用 async/await
let result;
for (const f of [func1, func2, func3]) {
    result = await f(result)
} // use last result (i.e. result3)


// eg3.
[func1, func2, func3]
    .reduce((p, f) => p.then(f), Promise.resolve())
    .then((result3) => {
        /* use result3 */
    })

    
// eg4. 可重用的 compose 函数
//      在 functional programming(函数式编程)中很常见
const applyAsync = (acc, val) => acc.then(val)
const composeAsync =
    (...funcs) =>
        (x) =>
            funcs.reduce(applyAsync, Promise.resolve(x))

在决定按“顺序”组合 promises 之前,需要考虑下是否真的有一个 promise 的执行依赖于另一个的结果。毕竟,相比“顺序”执行 promises,还是“同时”执行多个 promises 效率更高些。

4. 其它

4.1 解决回调地域

callback pyramid of doom,厄运的回调金字塔

回调地域,可以用 promise 的链式调用来解决。原理是:

function successCallback(result) {
  console.log(`Audio file ready at URL: ${result}`)
}

function failureCallback(error) {
  console.error(`Error generating audio file: ${error}`)
}

// 之前的写法,其中 audioSettings 函数里有异步操作 
createAudioFileAsync(audioSettings, successCallback, failureCallback)

// 有了 Promise 之后的写法,callbacks 被附加到了返回的 promise 对象上
createAudioFileAsync(audioSettings).then(successCallback, failureCallback)

举个例子:

// 回调地域
doSomething((result) => {
    doSomethingElse(result, (newResult) => {
        doThirdThing(newResult, (finalResult) => {
            console.log(`Got the final result: ${finalResult}`)
        }, failureCallback)
    }, failureCallback)
}, failureCallback)

// 链式调用,清晰简洁明了
doSomething()
    .then((result) => doSomethingElse(result))
    .then((newResult) => doThirdThing(newResult))
    .then((finalResult) => {
        console.log(`Got the final result: ${finalResult}`)
    })
    .catch(failureCallback)

Last updated