掌握 Async/Await

摘要: 還不用Async/Await就OUT了。。

Fundebug經授權轉載,版權歸原作者所有。

前端工程師肯定都經歷過 JS 回調鏈獄的痛苦過程,我們在使用 Promise 的時候總是不盡人意。這時候 Async/Await 應運而生,它到底有什麼魔力,我們來說道說道。

一、回顧 Promise

所謂 Promise,簡單說就是一個容器,裏面保存着某個未來纔會結束的事件(通常是一個異步操作)的結果。

1. 語法

new Promise(executor);
new Promise(function(resolve, reject) { ... });

2. 參數

帶有 resolve 、reject 兩個參數的一個函數。這個函數在創建 Promise 對象的時候會立即得到執行(在 Promise 構造函數返回 Promise 對象之前就會被執行),並把成功回調函數(resolve)和失敗回調函數(reject)作爲參數傳遞進來。調用成功回調函數(resolve)和失敗回調函數(reject)會分別觸發 Promise 的成功或失敗。

這個函數通常被用來執行一些異步操作,操作完成以後可以選擇調用成功回調函數(resolve)來觸發 Promise 的成功狀態,或者,在出現錯誤的時候調用失敗回調函數(reject)來觸發 Promise 的失敗。

3. Promise.all

Promise.all 來執行,all 接收一個數組參數,裏面的值最終都算返回 Promise 對象。這樣,三個異步操作的並行執行的,等到它們都執行完後纔會進到 then 裏面。

Promise.all([async1(), async2(), async3()])
.then(function(results){
  console.log(results);
});

all 會把所有異步操作的結果放進一個數組中傳給 then,就是上面的 results

4. Promise.race

all 方法的效果實際上是「誰跑的慢,以誰爲準執行回調」,那麼相對的就有另一個方法「誰跑的快,以誰爲準執行回調」,這就是 race方法:

Promise.race([requestImg(), timeout()])
.then(function(results){
    console.log(results);
})
.catch(function(reason){
    console.log(reason);
});

上述代碼演示了 race 的基本用法,實現的功能是:請求圖片,如果請求成功就返回圖片,否則就調用超時函數。

更多資源,請查看:

二、Promise 爲何不完美?

乍一看,Promise 還不錯,幫我們解決了回調鏈獄的問題。當然這只是簡單使用,碰到複雜的業務也有很雞肋的場景,比如:

1. 錯誤處理

在下面的 Promise 示例中,Try/Catch 不能處理 JSON.parse 的錯誤,因爲它在 Promise中。我們需要使用 catch,這樣錯誤處理代碼非常冗餘。並且,在我們的實際生產代碼會更加複雜。

const makeRequest = () => {
  try {
    getJSON().then(result => {
      // JSON.parse可能會出錯
      const data = JSON.parse(result)
      console.log(data)
    })
    // 取消註釋,處理異步代碼的錯誤
    // .catch((err) => {
    //   console.log(err)
    // })
  } catch (err) {
    console.log(err)
  }
}

Async/AwaitTry/Catch 可以同時處理同步和異步錯誤。使用 Async/Await 的話,Catch能處理 JSON.parse 錯誤:

const makeRequest = async () => {
  try {
    // this parse may fail
    const data = JSON.parse(await getJSON())
    console.log(data)
  } catch (err) {
    console.log(err)
  }
}

Async/Await 最讓人舒服的一點是代碼看起來是同步的。

2. 條件語句

下面示例中,需要獲取數據,然後根據返回數據決定是直接返回,還是繼續獲取更多的數據。

const makeRequest = () => {
  return getJSON()
    .then(data => {
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData => {
            console.log(moreData)
            return moreData
          })
      } else {
        console.log(data)
        return data
      }
    })
}

這些代碼看着就頭痛。嵌套(6層),括號,return 語句很容易讓人感到迷茫,而它們只是需要將最終結果傳遞到最外層的Promise。如果換成 Async/Await 呢:

const makeRequest = async () => {
  const data = await getJSON();
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData);
    return moreData;
  } else {
    console.log(data);
    return data;  
  }
}

所以,這纔是真正擺脫回調鏈獄的正確做法。

3. 中間值

你很可能遇到過這樣的場景,調用 promise1,使用 promise1 返回的結果去調用 promise2,然後使用兩者的結果去調用promise3。你的代碼很可能是這樣的:

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      return promise2(value1);
        .then(value2 => {        
          return promise3(value1, value2);
        })
    })
}
// 或者:
const makeRequest = () => {
  return promise1()
    .then(value1 => {
      return Promise.all([value1, promise2(value1)])
    })
    .then(([value1, value2]) => {      
      return promise3(value1, value2)
    })
}

怎麼寫都會覺得很複雜,那如果 Async/Await 用來實現呢,表現可能如下:

const makeRequest = async () => {
  const value1 = await promise1();
  const value2 = await promise2(value1);
  return promise3(value1, value2);
}

是不是很 6 ,將複雜的場景簡化,這樣的代碼就很有靈性了。

4. 錯誤棧

調用了多個 Promise,假設 Promise 鏈中某個地方拋出了一個錯誤,Promise 鏈中返回的錯誤棧沒有給出錯誤發生位置的線索。更糟糕的是,它會誤導我們;錯誤棧中唯一的函數名爲 callAPromise,然而它和錯誤沒有關係。(文件名和行號還是有用的)。

const makeRequest = () => {
  return callAPromise()
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => {
      throw new Error("oops");
    })
}

makeRequest().catch(err => {
  console.log(err);
  // output
  // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
})

然而,Async/Await 中的錯誤棧會指向錯誤所在的函數:

const makeRequest = async () => {
  await callAPromise();
  await callAPromise();
  await callAPromise();
  throw new Error("oops");
}

makeRequest().catch(err => {
  console.log(err);
  // output
  // Error: oops at makeRequest (index.js:7:9)
})

5. 調試

調試 Promise 有兩個問題:

  • 不能在返回表達式的箭頭函數中設置斷點;
  • 如果你在 then 代碼塊中設置斷點,調試器不會跳到下一個 then,因爲它只會跳過異步代碼;

而使用 Await/Async 時,你不再需要那麼多箭頭函數,這樣你就可以像調試同步代碼一樣跳過 Await 語句。

這裏只簡單的列出問題,詳細請查看原文:Async/Await 替代 Promise 的 6 個理由

三、新時代的曙光 Async/Await

簡單介紹:

  • Await/Async 是寫異步代碼的新方式,以前的方法有回調函數和 Promise。
  • Await/Async 是基於 Promise 實現的,它不能用於普通的回調函數。
  • Await/Async 與 Promise 一樣,是非阻塞的。
  • Await/Async 使得異步代碼看起來像同步代碼,這正是它的魔力所在。

使用 Promise 是這樣的:

const jarttoDemo = () =>
getJSON().then(data => {
  return data;
})

jarttoDemo();

使用 Async/Await 是這樣的:

const jarttoDemo = async () => {
  let data = await getJSON();
  return data;
}

jarttoDemo();

基本規則:

  • Async 表示這是一個 Async 函數,Await 只能用在這個函數裏面。
  • Await 表示在這裏等待 Promise返回結果了,再繼續執行。
  • Await 後面跟着的應該是一個 Promise 對象,當然,其他返回值也沒關係,只是會立即執行,不過那樣就沒有意義了。

四、更多用法示例

1. 簡單示例

var sleep = function (time) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve();
    }, time);
  })
};

var start = async function () {
    // 在這裏使用起來就像同步代碼那樣直觀
    console.log('start');
    await sleep(3000);
    console.log('end');
};

start();

2. 獲得返回值

var sleep = function (time) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
        // 返回 ‘ok’
        resolve('ok');
    }, time);
  })
};

var start = async function () {
    let result = await sleep(3000);
    console.log(result); // 收到 ‘ok’
};

3. 錯誤捕獲

const makeRequest = async () => {
  try {
    // this parse may fail
    const data = JSON.parse(await getJSON())
    console.log(data)
  } catch (err) {
    console.log(err)
  }
}

既然 then 不用寫了,那麼 catch 也不用寫,可以直接用標準的 try catch 語法捕捉錯誤。

var sleep = function (time) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      // 模擬出錯了,返回 ‘error’
      reject('error');
    }, time);
  })
};

var start = async function () {
  try {
    console.log('start');
    await sleep(3000); // 這裏得到了一個返回錯誤
    
    // 所以以下代碼不會被執行了
    console.log('end');
  } catch (err) {
    console.log(err); // 這裏捕捉到錯誤 `error`
  }
};

4. 條件語句

Promise 寫法:

const makeRequest = () => {
  return getJSON()
    .then(data => {
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData => {
            return moreData;
          })
      } else {
        return data;
      }
    })
}

Async/Await 寫法:

const makeRequest = async () => {
  const data = await getJSON();
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    return moreData;
  } else {
    return data;  
  }
}

5. 循環多個 Await

var start = async function () {
  for (let i = 1; i <= 10; i++) {
    console.log(`當前是第 ${i} 次等待..`);
    await sleep(1000);
  }
};

需要注意的是,Await 必須在 Async 函數的上下文中的。

6. 在 forEach 中使用

async function printFiles () {
  const files = await getFilePaths();

  for (let file of files) {
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  }
}

async function printFiles () {
  const files = await getFilePaths();

  await Promise.all(files.map(async (file) => {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
  }));
}

示例參考如下文章:

五、總結

我們一直在強調代碼的可讀性和可維護性,對我來說,Async/Await 更加易懂和易用。所以,不管是 Promise 還是 Async/Await ,能解決實際問題的技術就是好技術。

當然,Async/Await 也是基於 Promise 概念的,技術上我們也可以求同存異,不必太過較真。一句話,選擇權在你!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章