JavaScript 的 Async/Await 完勝 Promise 的六個理由

網路埋伏紀事發表於2017-06-03

提醒一下各位,Node 現在從版本 7.6 開始就支援 async/await 了。如果你還沒有試過它,這裡有一堆帶有示例的理由來說明為什麼你應該馬上採用它,並且再也不會回頭。

[編者按]:貌似嵌入 gist 上的程式碼在 medium 原生 app 中不行,但是在移動瀏覽器上可以。如果你是在 app 中讀本文,請點選共享圖示,選擇“在瀏覽器中開啟”,才看得到程式碼片段。

Async/await 101

對於那些從未聽說過這個話題的人來說,如下是一個簡單的介紹:

  • Async/await 是一種編寫非同步程式碼的新方法。之前非同步程式碼的方案是回撥和 promise。
  • Async/await 實際上是建立在 promise 的基礎上。它不能與普通回撥或者 node 回撥一起用。
  • Async/await 像 promise 一樣,也是非阻塞的。
  • Async/await 讓非同步程式碼看起來、表現起來更像同步程式碼。這正是其威力所在。

語法

假設函式 getJSON 返回一個promise,而該promise的完成值是一些JSON物件。我們只想呼叫它,並輸出該JSON,然後返回"done"

如下是用 promise 實現的程式碼:

const makeRequest = () =>  
  getJSON()  
    .then(data => {  
    console.log(data)  
  return "done"  
})  

makeRequest()

而這就是用async/await看起來的樣子:

const makeRequest = async () => {  
    console.log(await getJSON())  
    return "done"  
}  

makeRequest()

這裡有一些區別:

1.函式前面有一個關鍵字 asyncawait 關鍵字只用在用 async 定義的函式內。所有 async函式都會隱式返回一個 promise,而 promise 的完成值將是函式的返回值(本例中是 "done")。

2.上面一點暗示我們不能在程式碼的頂層用 await,因為這樣就不是在 async 函式內。

// 這段程式碼在頂層不能執行  
// await makeRequest()  

// 這段程式碼可以執行  
makeRequest().then((result) => {  
    // do something  
})

3.await getJSON() 意味著 console.log 呼叫會一直等待,直到 getJSON() promise 完成並列印出它的值。

為什麼 Async/await 更好?

1. 簡潔乾淨

看看我們少寫了多少程式碼!即使在上面那個人為的示例中,很顯然我們也是節省了不少程式碼。我們不必寫 .then,建立一個匿名函式來處理響應,或者給不需要用的變數一個名稱 data。我們還避免了程式碼巢狀。這些小小的優勢會快速累積起來,在後面的程式碼中會變得更明顯。

2. 錯誤處理

Async/await 會最終讓我們用同樣的結構( try/catch)處理同步和非同步程式碼變成可能。在下面使用 promise 的示例中,如果 JSON.parse 失敗的話,try/catch 就不會處理,因為它是發生在一個 prmoise 中。我們需要在 promise 上呼叫 .catch,並且重複錯誤處理程式碼。這種錯誤處理程式碼會比可用於生產的程式碼中的 console.log 更復雜。

const makeRequest = () => {  
  try {  
    getJSON()  
        .then(result => {  
        // this parse may fail  
        const data = JSON.parse(result)  
        console.log(data)  
    })  
    // uncomment this block to handle asynchronous errors  
  // .catch((err) => {  
    // console.log(err)  
    // })  
  } catch (err) {  
    console.log(err)  
  }  
}

現在看看用 async/await 實現的程式碼。現在 catch 塊會處理解析錯誤。

const makeRequest = async () => { 
  try { // 這個解析會失敗   
    const data = JSON.parse(await getJSON()) console.log(data) 
  } 
  catch (err) { 
    console.log(err)
  }
}

3. 條件句

假設想做像下面的程式碼一樣的事情,獲取一些資料,並決定是否應該返回該資料,或者根據資料中的某些值獲取更多的細節。

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  
        }  
  })  
}

這些程式碼看著就讓人頭疼。它只需將最終結果傳播到主 promise,卻很容易讓我們迷失在巢狀( 6 層)、大括號和返回語句中。

把這個示例用async / await 重寫,就變得更易於閱讀。

onst 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  
  }  
}

4. 中間值

你可能發現自己處於一種狀態,即呼叫你 promise1,然後用它的返回值來呼叫promise2,然後使用這兩個 promise 的結果來呼叫 promise3。你的程式碼很可能看起來像這樣:

const makeRequest = () => {  
  return promise1()  
    .then(value1 => {  
        // do something  
          return promise2(value1)  
            .then(value2 => {  
            // do something  
                return promise3(value1, value2)  
        })  
      })  
}

如果 promise3 不需要 value1,那麼很容易就可以把 promise 巢狀變扁平一點。如果你是那種無法忍受的人,那麼可能就會像下面這樣,在一個 Promise.all中包含值 1 和 2,並避免更深層次的巢狀:

onst makeRequest = () => {  
    return promise1()  
        .then(value1 => {  
            // do something  
            return Promise.all([value1, promise2(value1)])  
        })  
    .then(([value1, value2]) => {  
        // do something  
        return promise3(value1, value2)  
    })  
}

這種方法為了可讀性而犧牲了語義。除了為了避免 promise 巢狀,沒有理由將 value1value2併入一個陣列。

不過用 async/await 的話,同樣的邏輯就變得超級簡單直觀了。這會讓你對你拼命讓 promise 看起來不那麼可怕的時候所做過的所有事情感到懷疑。

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

5. 錯誤棧

假如有一段鏈式呼叫多個 promise 的程式碼,在鏈的某個地方丟擲一個錯誤。

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)  
})

從 promise 鏈返回的錯誤棧沒有發現錯誤發生在哪裡的線索。更糟糕的是,這是誤導的;它包含的唯一的函式名是callAPromise,它完全與此錯誤無關(不過檔案和行號仍然有用)。

但是,來自async / await的錯誤棧會指向包含錯誤的函式:

const makeRequest = async () => {  
    await callAPromise()  
    await callAPromise()  
    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)  
})

當在本地環境中開發並在編輯器中開啟檔案時,這不是啥大事,但是當想搞清楚來自生產伺服器的錯誤日誌時,就相當有用了。在這種情況下,知道錯誤發生在makeRequest中比知道錯誤來自一個又一個的 then 要好。

6. 除錯

最後但是同樣重要的是,在使用 async/await 時,一個殺手級優勢是除錯更容易。除錯 promise 一直是如此痛苦,有兩個原因:

1.沒法在返回表示式(無函式體)的箭頭函式中設定斷點。

試著在此處設定斷點

2.如果在.then塊中設定斷點,並使用像單步除錯這類除錯快捷方式,偵錯程式不會移動到後面的 .then ,因為它只單步除錯同步程式碼。

有了 async/await,我們就不再需要那麼多箭頭函式,您可以像正常的同步呼叫一樣單步除錯 await 呼叫。

總結

Async/await 是過去幾年中新增到 JavaScript 中的最具革命性的功能之一。它讓我們意識到 promise 的語法有多混亂,並提供了直觀的替代。

相關文章