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 的 value
是 undefined
(即上面提到的“promise chain 断了”)。在这种情况下,我们就有了两条独立的 promise chain,其中,里面的 chain 没人管,外面的 chain 还断了。
Copy 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 恢复的精度。比如:
Copy 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 的两种写法:
Copy 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 举例
看看下面的代码,有哪几处错误?
Copy doSomething()
.then((result) => {
doSomethingElse(result)
.then((newResult) => doThirdThing(newResult))
})
.then(() => doFourthThing())
点击查看答案有三处:
第 2 行的 promise 没有 return
not chain things together properly,通常发生在创建了一个新 promise 但忘了将其 return
后果就是:the chain is broken 或者说有了两个独立的 chains racing
修正后的代码如下:
Copy 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
语句。比如:
Copy 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
事件。
Copy // process.on() 可避免 error 输出到 console(控制台)
process.on('unhandledRejection', (reason, promise) => {
// Add code here to examine the 'promise' and 'reason' values
})
2.3 reject 写法
以下语句都可以触发 promise 的 reject 的逻辑:
Copy 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!'))
其中,被注释的语句是相对不推荐的。推荐的做法是:
正常返回用 reject()
或 Promise.reject()
。具体选哪个,取决于返回值的类型:
若返回值的类型是非 promise,则用前者,以便直接进入 rejected 状态的逻辑
若要返回 promise 类型,则用后者,毕竟它内部新建一个 promise 还是要花内存和时间的
出错用 throw new Error()
显式抛出错误,好处是:
能保留 stack trace(堆栈跟踪)等有价值的信息
可以避免 catch
部分做类型检查——判断是 String 还是 Error
3. 组合工具
composition tools
3.1 同时执行
3.2 顺序执行
顺序执行多个 promises,比如:
Copy // 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 的链式调用来解决。原理是:
Copy 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)
举个例子:
Copy // 回调地域
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)