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
性能方面的小顾虑:有一种说法是 throw
用多了会影响性能,因为代码有可能跳到不可预知的位置。但是结合 Promise 的实现 ,我们知道回调函数的执行都被包在了 try-catch
块里,所以代码的跳跃幅度是在可控范围之内的。因此,这里的性能损失可以忽略。
3. 组合工具
composition tools
3.1 同时执行
“同时”运行多个 promises ,不相互阻塞。
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)