從 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
圖片來源: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 的優點是:
- 更簡潔並易讀的寫法
- 不會陷入 Promise Chain 的地獄當中
PS: 目前 async/await 在除了 IE 的各瀏覽器皆有支援,
如果要在 IE 使用的話,需要安裝babel-polyfill
參考資料:
異步函數 - 提高 Promise 的易用性
JavaScript 编程语言 Promises, async/await
鐵人賽:JavaScript Await 與 Async
async 函數
理解 JavaScript 中的 macrotask 和 microtask