JavaScript非同步程式設計大冒險: Async/Await

Dominic_Ming發表於2017-12-20

Async/Await 是什麼?

Async/Await 也就是大家知道的非同步函式,它是一個用來控制 JavaScript 非同步流程的一個記號。而在很多現代瀏覽器上也曾實現過這樣的設想。它的靈感來源於C# 和 F#,現在 Async/Await 在ES2017已經平穩著陸。

通常我們認為 async function 是一個能返回 Promisefunction 。你也可以在 async function 使用 await 關鍵字。 await 關鍵字可以放在一個需要返回Promise的表示式前,所得到的值被從Promise裡面剝離開,以便能用更直觀的同步體驗。我們來看一下實際的程式碼更直觀。

// 這是一個簡單的返回 Promise 函式
// 功能是在兩秒以後 resolve("MESSAGE") .
function getMessage() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve("MESSAGE"), 2000);
  });
}
複製程式碼
async function start() {
  const message = await getMessage();
  return `The message is: ${message}`;
}
複製程式碼
start().then(msg => console.log(msg));
// "The message is: MESSAGE"
複製程式碼

為什麼要用 Async/Await?

Async/Await 提供了一個看起來相對同步的方法來執行非同步程式碼。同時也提供了一種簡潔而直觀的方法來處理非同步的錯誤,因為它實現了try…catch 標記,這是JavaScript裡面最常見的一種同步模式。

在我們開始冒險之前,我們應該清楚,Async/Await 是建立在 JavaScript Promises 上的,而且關於它的知識是很重要的。

關於記號

Async 函式

要建立一個 async 函式,一般就要把 async 關鍵字放在宣告函式之前,就像這樣:

async function fetchWrapper() {
  return fetch('/api/url/');
}

const fetchWrapper = async () => fetch('/api/url/');
複製程式碼
const obj = {
  async fetchWrapper() {
    // ...
  }
}
複製程式碼

Await 關鍵字

async function updateBlogPost(postId, modifiedPost) {
  const oldPost = await getPost(postId);
  const updatedPost = { ...oldPost, ...modifiedPost };
  const savedPost = await savePost(updatedPost);
  return savedPost;
}
複製程式碼

在這裡的 await 是用在其他返回 promise 的函式前。在第一行,oldPost被賦值為getPost執行resolve後返回的value。在下一行,我們使用瞭解構賦值來演示怎樣把 oldPost 和 modifiedPost 合併。最終我們把 post 儲存下來,返回了 savedPost 的結果。

示例 / FAQ

?️“到底怎麼處理錯誤?”

這是一個好問題!當你使用 async/await 的時候,你也可以使用 try...catch 。在下面展示了,我們非同步的 fetch 了一些東西,返回了某種錯誤,我們可以在catch裡面拿到錯誤。

async function tryToFetch() {
  try {
    const response = await fetch('/api/data', options);
    return response.json();
  } catch(err) {
    console.log(`An error occured: ${err}`);
    // 比起返回一個錯誤
    // 我們可以返回一個空的data
    return { data: [] };
  }
}
複製程式碼
tryToFetch().then(data => console.log(data));
複製程式碼

?️ ️“我還是不知道為什麼 async/await 比 callbacks/promises 好.”

很高興你問了這個問題,這裡有一個例子可以說明不同。我們這裡只是想要非同步的 fetch 一些資料,然後得到資料後,簡單的返回一些經過處理的data,如果有錯誤,我們簡單的只是想要返回一個物件。

// 我們這裡有 fetchSomeDataCB, 和 processSomeDataCB
// NOTE: CB 代表 callback

function doWork(callback) {
  fetchSomeDataCB((err, fetchedData) => {
    if(err) {
      callback(null, [])
    }

    processSomeDataCB(fetchedData, (err2, processedData) => {
      if(err2) {
        callback(null, []);
      }

      // return the processedData outside of doWork
      callback(null, processedData);
    });
  });
}

doWork((err, processedData) => console.log(processedData));
複製程式碼
// 我們這裡有 fetchSomeDataP, 和 processSomeDataP
// NOTE: P 意味著這個函式返回一個 Promise

function doWorkP() {
  return fetchSomeDataP()
    .then(fetchedData => processSomeDataP(fetchedData))
    .catch(err => []);
}

doWorkP().then(processedData => console.log(processedData));
複製程式碼
async function doWork() {
  try {
    const fetchedData = await fetchSomeDataP();
    return processSomeDataP(fetchedData);
  } catch(err) {
    return [];
  }
}

doWork().then(processedData => console.log(processedData));
複製程式碼

Callback vs Promise vs Async/Await

?️“他的併發性如何”

當我們需要有順序的做一些事情,我們通常用await一個一個宣告所有的步驟。在這之前為了理解併發,我們必須使用Promise.all。如果我們現在有三個非同步動作需要平行執行,在加上await之前,我們需要讓所有的Promise先開始。

// 這不是解決方法,他們會逐個執行
async function sequential() {
  const output1 = await task1();
  const output2 = await task2();
  const output3 = await task3();
  return combineEverything(output1, output2, output3);
}
複製程式碼

因為上述程式碼只是依次的執行了三個任務,而沒有併發的執行,後一個會依賴前一個執行完成。所以我們要改造成Promise.all 的方式。

// 這就可以併發的執行
async function parallel() {
  const promises = [
    task1(),
    task2(),
    task3(),
  ];
  const [output1, output2, output 3] = await Promise.all(promises);
);
  return combineEverything(output1, output2, output3);
}
複製程式碼

在這個例子上,我們首先執行了3個非同步的任務,之後把Promise都儲存進了一個array。我們使用 Promise.all 來完成來全部併發結果的收集。

另外的一些提示

  • 你很容易會忘記每次你 await 一些程式碼, 你需要宣告這個函式是一個 async function
  • 當你使用 await 時候,它值暫停了所涉及的 async function 。 換句話說,下面的程式碼會在其他東西log之前log 'wanna race?'
const timeoutP = async (s) => new Promise((resolve, reject) => {
  setTimeout(() => resolve(s*1000), s*1000)
});
複製程式碼
[1, 2, 3].forEach(async function(time) {
  const ms = await timeoutP(time);
  console.log(`This took ${ms} milliseconds`);
});
複製程式碼
console.log('wanna race?');
複製程式碼

當你的第一個await 的 promise在主執行緒上返回執行了結果,在forEach外面的log不會被阻塞。

瀏覽器支援

看一看這張瀏覽器支援表

Node 支援

node 7.6.0 以及以上版本支援 Async/Await !

作者:Benjamin Diuguid

原文:Asynchronous Adventures in JavaScript: Async/Await

翻譯:Dominic Ming

相關文章