大部分同學瞭解Promise,也知道async await可以實現同步化寫法,但實際上對一些細節沒有理解到位,就容易導致實際專案中遇到問題。開始先拋結論,下文將針對主要問題點進行論述。1、所有async方法呼叫,必須加await或catch,捕獲錯誤(等待就用await,無需等待就用catch);如果最上層的async方法是被框架(react、egret)呼叫的,無法加await,則需要在這個async方法內做好try catch,不要把報錯拋到框架層;2、async方法,實際返回了一個promise,預設把return值作為promise的resolve內容,而報錯則封裝為promise的reject;3、async方法內那麼遇到異常要終止,可以直接throw ‘xxx’/Error;4、async方法內如果有呼叫下一層方法(這個方法是async方法或返回Promise),則需要加await,等待這個promise結果;如果同時要返回該下層呼叫的return值,則可以省略await,改為直接return這個Promise(但不建議,還是統一await同步寫法比較好理解,詳見下文例子);5、async方法如果正常執行,則直接執行完,return即可,不需要自行建立一層promise。
1. 為什麼async方法一定要加await或catch?
這裡,需要先看一個例子,大家看看有什麼問題。
main(); async function main() { try { loadImage(); loadConfig(); } catch (e) { console.log('main', e); } } function loadImage(){ return new Promise((resolve, reject) => { setTimeout(reject, 1000, 'network error'); }); } async function loadConfig(){ throw 'logic bug'; await wait(); console.log('config ok'); } function wait(){ return new Promise((resolve, reject) => { setTimeout(resolve, 1000); }); }
。
。
。
。
答案公佈:
無法捕獲loadImage和loadConfig的報錯。
上述程式碼是一個典型,實際是從專案某個同學程式碼中抽象得來的。雖然看起來很工整很穩健,try catch做的很到位,但實際上,他沒有把async和await理解透徹,沒有理解到async返回的是Promise,無論是async內同步的報錯還是非同步(延遲)的報錯,對上層呼叫來說,都是一個微任務。
要解決上述問題,關鍵點就是,呼叫loadImage和loadConfig時,加await。
async function main() { try { await loadImage(); await loadConfig(); } catch (e) { console.log('main', e); } }
所以,呼叫async方法,不加await,就類似一個耍流氓行為,等同於使用Promise但不加catch。
另外,最頂層的方法main再被呼叫時,由於沒有包裹在async內,無法使用await,此時我們可以在main()後加上catch(),因為async方法實際返回的是Promise。題外話:目前top-level await還沒有正式成為標準,但最新V8引擎裡邊已經可以使用(https://v8.dev/features/top-level-await,https://github.com/tc39/proposal-top-level-await)
2. 為什麼async方法內不要return Promise?
先看一個典型的例子
async function main() { try { const result = await load(url); //... } catch (e) { console.error(e); } } async function load(url) { if (!url) { return Promise.reject('url is invalid'); } else { const result = await fetch(url); //代表一個非同步操作 return Promise.resolve(result); } }
大家再看看這段程式碼是否有問題?
。
。
。
。
。
答案公佈:
執行時,實際沒有問題,邏輯是正常的,也能捕獲錯誤。但是,有一些不足,多了一層Promise,會導致效能下降(新版本chrome解決了),而且影響回撥執行時機。
接下來通過兩個程式碼對比一下,大家會更清楚。
程式碼片段1
console.log('script start'); async function async1() { await async2(); console.log('async1 end'); } async function async2() { console.log('async2 end'); } async1(); setTimeout(function() { console.log('setTimeout'); }, 0); new Promise(resolve => { console.log('Promise'); resolve(); }).then(function() { console.log('promise end'); }); console.log('script end');
程式碼片段2
console.log('script start'); async function async1() { await async2(); console.log('async1 end'); } async function async2() { console.log('async2 end');
return Promise.resolve().then(()=>{ console.log('async2 end in promise') }) } async1(); setTimeout(function() { console.log('setTimeout'); }, 0); new Promise(resolve => { console.log('Promise'); resolve(); }).then(function() { console.log('promise end'); }); console.log('script end');
對比一下chrome控制檯執行結果:
左(片段1) 右(片段2)
不同點就是,async1中await async2的時間推遲了,排在另外一個promise微任務之後。
通過這例子可見,雖然async方法裡邊return一個Promise和直接return 值 並沒有明顯的差異,但會在呼叫時機上產生一些微妙的變化。
所以,總體來說,不建議在async方法中再return或reject一個Promise。
3. 參考寫法
最後,綜合上述結論,提供一些參考寫法,大家可以按需取用。
main().catch(()=>{}); // 頂層呼叫,如果沒有async包裹就用catch,如果是框架內呼叫,則在main函式體中做好catch async function main() { try { const result = await load(url); //... } catch (e) { // 所有try內的async方法均有await,所有錯誤都會層層丟擲,直到這裡捕獲 console.error(e); } } async function load(url) { if (!url) { throw 'url is invalid'; // 直接throw錯誤資訊,簡潔明瞭,直接中斷後續流程 } const config = await fetch(url); // 假如fetch介面是一個網路獲取,接收url,返回一個Promise return await runTask(config); //代表一個非同步操作 // return runTask(config); // 和上一行,兩種做法都可以,這裡是return語句,可以把promise當做async方法的return值,上層await會解開。但為了方便記憶,不建議使用這個方式,應該統一使用await。 } async function runTask(data) { // 對接一個不支援Promise的第三方庫,我們只需要在最下層方法,包一個promise return new Promise((resolve, reject) => { thirdPartyRun(data, (res) => { resolve(res); // 這裡返回資料 }, (e) => { reject(e); // 這裡可以做一些錯誤資訊轉換 }); }); } // 代表一個不支援Promise的第三方庫,如何對接到async await體系 function thirdPartyRun(data, success, fail) { //... }