async await 你真的用對了嗎?

Kenko發表於2020-11-20
大部分同學瞭解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-awaithttps://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) {
  //...
}

 

相關文章