吳嘉豐 (Joseph) 已發佈 2019-10-28

從 JavaScript Promise 到 Async Await

提到 Async Await 我們知道他是 JavaScript 在 ES7 當中,是 Promise 的語法糖,
讓我們來看看跟 Promise 比起來,有什麼樣的差別吧!

以下是原本在 來點 JavaScript 的 Promise 中的例子:

function getToken() {
    return new Promise(function(resolve, reject) {
        const states = Math.random() > 0.5 ? true : false
            setTimeout(function() {
                if (states) {
                    resolve('順利取餐')
                } else {
                    reject('取不到餐')
                }
            }, 3000)
    })
}

getToken()
    .then(res => console.log(res))  // 順利取餐
    .catch(err => console.log(err)) // 取不到餐

我們可以用 Async Await 來改寫並有一樣的結果

let promise = new Promise(function(resolve, reject) {
    let states = Math.random() > 0.5 ? true : false
    console.log(states)
    setTimeout(function() {
                if (states) {
                    resolve('順利取餐')
                } else {
                    reject('取不到餐')
                }
            }, 3000)
})

async function getToken() {
    try {
        const fufillValue = await promise
        console.log(fufillValue)
    } catch (err) {
        console.log(err)
    }
}

getToken() // 順利取餐 or // 取不到餐

我們從上面分別用 Promise 和 Async Await 寫的 code 來看,我們可以發現,我們不用再使用 .then 和 .catch 的 Promise Chain 來得到一個 resolve 或 reject 的結果,取代的是用 async 和 await 還有 try catch 來得到我們要的結果。

Async / Await 的意思

我們首先來看一下 MDN 對於 async function 的定義,

async function 宣告被定義為一個回傳 AsyncFunction 物件的非同步函式 。

async function example(url) {
    let getData = await fetch(url)
    console.log(await getData.json())
}

example('https://dog.ceo/api/breeds/image/random')

意思是什麼呢?如果我們宣告了上面的 example function,我們在 example function 前加上一個 async,表示 example function 他是一個非同步函式,當他被執行時,會回傳一個 Promise 的 resolve 狀態,而有例外狀態或失敗時,則會回傳一個 reject 狀態。

而在上面的 code 當中,我們也使用了 await 這個 API,他的意思就等同於 Promise 的 .then 可以等待非同步完成,就會回應得到的值。

Async 的內容皆會視為 resolve

在 async function 中,如果回應的值不是 Promise,async function 也會視為 Promise resolve 而回傳。

async function normal() {
    console.log(await 1)
}

normal() //1

當 Async 發生 reject 或產生例外時

但是如果在 async function 中,有例外或錯誤的情形發生時,
都會拋出錯誤的狀態。

  • reject
async function wrong() {
    await Promise.reject(new Error("錯了"));
}

wrong() // Uncaught (in promise) Error: 錯了
  • 例外處理
async function wrong() {
    throw new Error("錯了")
}

wrong() // Uncaught (in promise) Error: 錯了

使用 try catch 做錯誤和例外處理

在 Promise 時,我們會使用 catch 來做例外和錯誤處理,
在 async function 中的話,就使用 try catch 來處理。

async function wrong() {
    try {
        let response = await fetch('https://dog.ceo/xxx')
        let data = await response.json()
        console.log(data)
    } catch (err) {
        console.log(err)
    } // TypeError: Failed to fetch
}

wrong()

但如果同時有兩個 await 的 response,一個成功一個失敗呢?

async function wrong() {
    try {
        let res = await fetch('https://dog.ceo/api/breeds/image/random')
        let data = await res.json()
        console.log(data)
        let res2 = await fetch('https://dog.ceo/xxx')
        let data = await res2.json()
        console.log(data2)
    } catch (err) {
        console.log(err)
    }
}

wrong()
//{message: "https://images.dog.ceo/breeds/saluki/n02091831_4846.jpg", status: "success"}
//TypeError: Failed to fetch

如上面範例,這時候便會依照成功或失敗的順序,如果先成功了便會回傳成功的值,接著會回傳錯誤的訊息。但如果是失敗在前面,因為在失敗的回應就發生錯誤,之後的成功的程式便不會執行。

和 Promise.all 使用

async function 也可以和 Promise.all 一起使用,
當如果有個 promise 錯誤時,也可以用 try catch 做例外處理。

都成功時:

async function ex() {
    let results = Promise.all([
        fetch('https://dog.ceo/api/breeds/image/random'),
        fetch('https://aws.random.cat/meow'),
    ])
    try {
        const [a, b] =  await results
        console.log(await a.json(), await b.json())
    } catch (err) {
        console.log(err)
    }
}

ex()

//{message: "https://images.dog.ceo/breeds/terrier-westhighland/n02098286_6041.jpg", status: "success"}
//{file: "https://purr.objects-us-east-1.dream.io/i/PndM9.jpg"}

有一個失敗時:

async function ex() {
    let results = await Promise.all([
        fetch('https://dog.ceo/api/breeds/image/random'),
        fetch('https://aws.random.cat/meow1'),
    ])
    try {
        const [a, b] = results
        console.log(await a.json(), await b.json())
    } catch (err) {
        console.log(err)
    }
}

ex()
//Uncaught (in promise) TypeError: Failed to fetch

macrotask 和 microtask

在看 JavaScript 教程時,在 Microtask queue 的段落看到了一個有趣的例子。

在以下的範例中,alert 的順序時?

async function f() {
    return 1
}

;(async () => {
    setTimeout(() => alert('timeout'), 0)

    await f()

    alert('await')
})()

答案是:

// 'await'
// 'timeout'

你會說奇怪,不是 setTimeout 先執行,為什麼反而先回應的是 await f(),這個就要說到 JavaScript 的 event loop。

我們先看以下的程式碼和圖片。

function main() {
    console.log('main')
} //(1)

function showText() {
    console.log('showText')
} //(2)

setTimeout(function() {
    console.log('setimeout')
}, 0) //(3)

function another() {
    console.log('another')
} //(4)

main() //(5)
showText() //(6)
another() //(7)

//main
//showText
//another
//setimeout

image
圖片來源:JavaScript 中事件循環和 Node.js 中事件循環

我們可以看上面的程式碼和圖片對照看,因為 JavaScript 引擎是單執行緒,所以在 stack 中一次只能執行一個 main thread,所以我們看上面的程式碼部分,會依序由 (1) -> (2) -> (3) -> (4) 放入上面圖片的 stack,但是由於 setTimeout 是 Web API,當他與 event 或 ajax 被放入 stack 時,他會先被放到圖片中的 Web api 去處理,等時間到了,或 event 被觸發就會丟到底下的 callback queue 去排隊,等待程式碼中,(5)、(6)、(7)被執行,然後 stack 空了的時候,這時候放在底下 callback queue 的 setTimeout (3) 便會放到 stack 裡面,並執行 setTimeout 裡面的 callback function。

另外如果是 setTimeout 和 promise 的 then,雖然同樣會被放到 callback queue 裡面,但是 setTimeout 會被放到 macrotask 就是一般說的 callback queue,然後 promise 的 then 會被放到 microtask ,但是因為 mircrotask 的優先度會大於 macrotask,所以會先執行 mircrotask 裡的 then,且 macrotask 是一次執行一個任務,需要排隊,但是 mircrotask 是會一個接一個執行到 mircrotask 裡面沒隊列為止。

那我們再回來看剛剛提到的 async function,

async function f() {
    return 1
}

(async () => {
    setTimeout(() => alert('timeout'), 0)

    await f()

    alert('await')
})()

在這個 async function 中,因為 setTimeout(() => alert('timeout'), 0); 是 macrotask 優先度比 await f(); 是 macrotask 的優先度低 (async function f() 為 promise then 的語法糖 ),所以 stack 空的時候會先執行 f(),再執行 setTimeout 裡面的 callback function。

推薦閱讀:What the heck is the event loop anyway?
JS runtime 動畫

總結

總結來說 Async/Await 的優點是:

  1. 更簡潔並易讀的寫法
  2. 不會陷入 Promise Chain 的地獄當中

PS: 目前 async/await 在除了 IE 的各瀏覽器皆有支援,
如果要在 IE 使用的話,需要安裝babel-polyfill

can i use 關於 async/await 的支援度

參考資料:

異步函數 - 提高 Promise 的易用性
JavaScript 编程语言 Promises, async/await
鐵人賽:JavaScript Await 與 Async
async 函數
理解 JavaScript 中的 macrotask 和 microtask

關於筆者

暱稱:吳嘉豐 (Joseph)

介紹:轉職前端,喜歡研究前端技術的工程師,平時出沒在各個前端交流的社群

linkedin

文章列表 文章列表