[ES6] async/await 應用指南

async/await 是什麼

async/await 是 ES7 引入的新的異步代碼 規範,它提供了一種新的編寫異步代碼的方式,這種方式在語法層面提供了一種形式上很是接近於同步代碼的異步非阻塞代碼風格,在此以前咱們使用的可能是異步回調、 Promise 模式。
從實現上來看 async/await 是在 生成器、Promise 基礎上構建出來的新語法:以 生成器 實現流程控制,以 Promise 實現異步控制。
Node 自 v8.0.0 起已經徹底支持 async/await 語法,babel 也已經徹底支持 async/await 語法的轉譯。javascript

下面,咱們以一個一個實例的方式,由淺入深介紹 async/await 語法的使用。java

一個簡單的實例

咱們來實現一個獲取登陸用戶信息的函數,邏輯以下:node

  1. 獲取用戶登陸態
  2. 若是用戶已經登陸,返回對應的用戶信息
  3. 若是用戶未登陸,跳轉到登陸頁

以回調方式實現

回調 在最第一版本的 JS 就已經出現,可謂歷史悠久,到如今也還保持着至關的活力。
若是以回調方式實現上述需求,代碼大概以下:babel

function getProfile(cb) {
  isUserLogined(req.session, (err, isLogined) => {
    if (err) {
      cb(err);
    } else if (isLogined) {
      getUser(req.session, (err, profile) => {
        if (err) {
          cb(err);
        } else {
          cb(null, profile);
        }
      });
    } else {
      cb(null, false);
    }
  });
}

感覺到臭味了嗎?這裏咱們還只是實現了兩層的異步調用,代碼中就已經有許多問題,好比重複的 if(err) 語句;好比層層嵌套的函數。
另外,若是在層層回調函數中出現異常,調試起來是很是讓人奔潰的 —— 因爲 try-catch 沒法捕獲異步的異常,咱們只能不斷不斷的寫 debugger 去追蹤,簡直步步驚心。
這種層層嵌套致使的代碼臭味,被稱爲 回調地獄,在過去是困惑社區的一個大問題。session

以 Promise 方式實現

Promise 模式最先只是社區出現的一套解決方案,但憑藉其優雅的鏈式調用語句,獲得愈來愈多人的青睞,最終被列爲 ES6 的正式規範。
上面的需求,若是以 Promise 模式實現:異步

function getProfile() {
  return isUserLogined(req.session)
    .then(isLogined => {
      if (isLogined) {
        return getUser(req.session);
      }
      return false;
    })
    .catch(err => {
      console.log(err);
    });
}

ok,這減小了些模板代碼,也有了一致的異常 catch 方案。但這裏面也有其餘的一些坑,好比,若是咱們要 resolve 兩個不一樣 Promise 的值?假設上面的例子中,咱們還須要返回用戶的日誌記錄:async

function getProfile() {
  return isUserLogined(req.session)
    .then(isLogined => {
      if (isLogined) {
        return getUser(req.session).then(profile => {
          return getLog(profile).then(logs => Promise.resolve(profile, logs));
        });
      }
      return false;
    })
    .catch(err => {
      console.log(err);
    });
}

上面的代碼在 getUser.then 中嵌套了一層 getLog.then ,這在代碼上破壞了 Promise 的鏈式調用法則,並且,getUser.then 函數中發生的異常是沒法被外層的 catch 函數捕獲的,這破壞了異常處理的一致性。函數

Promise 的另外一個問題,是在 catch 函數中的異常堆棧不夠完整,致使難以追尋真正發生錯誤的位置。好比如下代碼中:oop

function asyncCall(){
    return asyncFunc()
      .then(()=>asyncFunc())
      .then(()=>asyncFunc())
      .then(()=>asyncFunc())
      .then(()=>throw new Error('oops'));
}

asyncCall()
  .catch((e)=>{
    console.log(e);
    // 輸出:
    // Error: oops↵    at asyncFunc.then.then.then.then (<anonymous>:6:22)
  });

因爲拋出異常的語句是在一個匿名函數中,運行時會認爲錯誤發生的位置是 asyncFunc.then.then.then.then,假如代碼中大量使用了 asyncFunc 函數,那麼上面的報錯信息就很難幫助咱們準肯定位錯誤發生的位置。
咱們固然能夠給每一個 then 的回調函數賦予一個有意義的名詞,但這又喪失了箭頭函數、匿名函數的簡潔。性能

以 async/await 方式實現

最後,終於輪到咱們此次的主題 —— async/await 方式的異步代碼,雖然這是一個 ES7 規範,但配合強大的 babel,如今已經能夠大膽使用。
以上需求的實現代碼:

async function getProfile() {
  const isLogined = await isUserLogined(req.session);
  if (isLogined) {
    return await getUser(req.session);
  }
  return false;
}

代碼比上面兩種風格要簡單了許多,形式上就是同步操做流程,與咱們的需求描述也很是很是的接近。

async 關鍵字用於聲明一個函數是異步的,能夠出如今任何函數聲明語句中,包括:普通函數、箭頭函數、類函數。普通函數的 constructorFunction, 而被 async 關鍵字修飾的函數則是 AsyncFunction 類型的:

Object.getPrototypeOf(function() {}).constructor;
// output
// Function() { [native code] }

Object.getPrototypeOf(async function() {}).constructor;
// output
// AsyncFunction() { [native code] }

await 關鍵字只能在 async 函數中使用,用於聲明一個異步調用,好比上面例子中的 const isLogined = await isUserLogined(req.session);,當 async 風格的 getProfile 函數執行到該語句時,會掛起當前函數,將後續語句加入到 event loop 循環中,這一點與 生成器 執行特性相同。
直到 isUserLogined 函數 resovle 後,才繼續執行後面的語句。

咱們能夠在 async 函數中編寫任意數量的 await 語句,async 函數的執行會一直處在 執行-掛起-執行 的循環中,這種特性獲得了語言層面的支持,並不須要咱們爲此編寫多餘的代碼,這就爲複雜的異步場景提供便捷的實現方案,好比:

async function asyncCall() {
  const v1 = await asyncFunc();
  const v2 = await asyncFunc(v1);
  const v3 = await asyncFunc(v2);
  return v3;
}

到這裏,咱們已經簡單瞭解了 async/await 的用法,這種同步風格的異步處理方案,相比而言會更容易維護。

async 中的異常處理

上面咱們提到,在 Promise 模式中,catch 函數難以得到完整的異常信息,致使在 Promise 下作調試變得困難重重,那在 async/await 中呢?
咱們來看一段代碼:

async function asyncCall() {
  try {
    await asyncFunc();
    throw new Error("oops");
  } catch (e) {
    console.log(e);
    // output
    // Error: oops  at asyncCall (<anonymous>:4:11)
  }
}

相比 Promise 模式,上面代碼中異常發生的位置是 asyncCall 函數!相對而言,容易定位了許多。

並聯的 await

async/await 語法確實很簡單好用,但卻容易用岔了。如下面代碼爲例:

async function retriveProfile(email) {
  const user = await getUser(email);
  const roles = await getRoles(user);
  const level = await getLevel(user);
  return [user, roles, level];
}

上面代碼實現了獲取用戶基本信息,而後經過基本信息獲取用戶角色、級別信息的功能,其中 getRolesgetLevel 二者之間並沒有依賴,是兩個並聯的異步操做。
但代碼中 getLevel 卻須要等待 getRoles resolve 以後才能執行。並非全部人都會犯這種錯誤,而是同步風格很容易誘惑咱們忽略掉真正的異步調用次序,而陷入過於簡化的同步思惟中。寫這一段的目的正是爲了警醒你們,async 只是形式上的同步,根本上仍是異步的,請注意不要讓使用者把時間浪費在無謂的等待上。
上面的邏輯,用一種稍微 一些的方式來實現,就能夠避免這種性能損耗:

async function retriveProfile(email) {
    const user = await getUser(email);
    const p1 = getRoles(user);
    const p2 = getLevel(user);
    const [roles, levels] = await Promise.all(p1, p2);
    return [user, roles, levels];
}

注意,代碼中的 getRolesgetLevel 函數都沒有跟在 await 關鍵字以後,而是把函數返回的 Promise 存放在變量 p1p2 中,後續纔對 p1p2 執行 await 聲明, getRolesgetLevel 就能同時執行,不需等待另外一方的完成。

這個問題在循環場景下特別容易發生,假設咱們須要獲取一批圖片的大小信息:

async function retriveSize(imgs) {
  const result = [];
  for (const img of imgs) {
    result.push(await getSize(img));
  }
}

代碼中的每次 getSize 調用都須要等待上一次調用完成,一樣是一種性能浪費。一樣的功能,用這樣的方式會更合適:

async function retriveSize(imgs) {
  return Promise.all(imgs.map(img => getSize(img)));
}

這實際上已經回退到了 Promise 模式,因此爲了寫出良好的 async/await 代碼,建議仍是認真學習學習 Promise 模式

相關文章
相關標籤/搜索