吳嘉豐 (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 來得到一個 resolvereject 的結果,取代的是用 asyncawait 還有 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。

另外如果是 setTimeoutpromisethen,雖然同樣會被放到 callback queue 裡面,但是 setTimeout 會被放到 macrotask 就是一般說的 callback queue,然後 promisethen 會被放到 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

文章列表 文章列表