其他章節請看:

es6 快速入門 系列

async

前文我們已經知道 promise 是一種非同步程式設計的選擇。而 async 是一種用於執行非同步任務更簡單的語法。

Tip:建議學完 Promise 在看本文。

async 函式

async 函式是使用 async 關鍵字宣告的函式。就像這樣:

async function fa(){

}

async 函式可以看作由多個非同步操作包裝成的一個 Promise 物件。

async 函式返回 Promise

async 函式總是返回一個 Promise 物件。如果一個 async 函式的返回值看起來不是 promise,那麼它將會被隱式地包裝在一個 promise 中。請看示例:

async function fa() {
return 1
} // 等價於 function fa() {
return Promise.resolve(1)
} console.log( fa() instanceof Promise) // true

即使 async 方法中沒有顯示的 return ,async 方法仍會返回 Promise。請看示例:

async function fa() {}
console.log( fa() instanceof Promise)

fa 方法等價於:

function fa() {
return Promise.resolve()
}

async 函式多種形式

async 函式有多種使用形式。例如:

// 函式表示式
const fa = async funciton() {}; // 物件的方法
let obj = {async foo(){}} // Class 的方法
class Dog{
async say(){}
} // 箭頭函式
const fa = async () => {}

形式雖然很多,但都是在函式前面增加 async 關鍵字。

async 函式中的 return

async 函式內的 return 返回值,會成為 then() 方法回撥函式的引數。請看示例:

async function foo() {
return 'hello'
} foo().then(v => {
console.log(v)
}) // hello

async 函式內部丟擲的錯誤會導致返回的 Promise 物件變為 reject 狀態。丟擲的錯誤物件會被 catch 方法回撥接收到。請看示例:

async function foo() {
throw new Error('fail')
return 'hello'
} foo().catch(v => {
console.log(v.message)
}) // fail

Promise 物件的狀態變化

async 函式返回的 Promise 物件必須等到內部所有 await 命令後的 Promise 物件執行完才會發生狀態變化,除非遇到 return 語句,或者丟擲錯誤才會立刻結束。請看示例:

function createPromise(val, time = 1000){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(val)
resolve(val)
}, time)
})
} async function foo() {
let a = await createPromise(1)
let b = await createPromise(2)
return {a, b}
} foo().then(v => {
console.log(v)
}, v => {
console.log(v.message)
}) /*
1
2
{ a: 1, b: 2 }
*/

這段程式碼需要 2 秒,等待內部兩個 Promise 狀態都置為已完成,才會輸出 { a: 1, b: 2 }

如果遇到 return 或者丟擲錯誤,則會立即結束。就像這樣:

async function foo() {
// 遇到 return
return 1
// 或丟擲錯誤
// throw new Error('fail')
let a = await createPromise(1)
let b = await createPromise(2)
return {a, b}
}

await

asyn 函式可能包含 0 個或多個 await 表示式。就像這樣:

async function fa() {
return await 1
}

await 表示式會暫停整個 async 函式的執行程序並出讓其控制權,只有當其等待的基於 promise 的非同步操作被兌現或被拒絕之後才會恢復程序。promise 的解決值會被當作該 await 表示式的返回值。

await 的返回值

首先看一段程式碼:

async function fa() {
const result = await 1
return result
} fa().then(v => {
console.log(v)
}) // 1

為什麼 result 是 1?

首先,因為 await 命令後面是一個 Promise 物件。如果不是,會被轉為一個立即 resolve 的 Promise 物件。所以下面 fa() 方法是相等的:

async function fa() {
return await 1
} // 等價於 async function fa() {
return await Promise.resolve(1)
}

而在 Promise 中所學,我們知道 fa() 方法又等於如下程式碼:

async function foo() {
return await new Promise((resolve, reject) => {
resolve(1)
})
}

其次,Promise 的解決值會被當作該 await 表示式的返回值。所以 result 等於 1。

如果刪除 fa() 方法中的 return,將輸出 undefined。請看示例:

async function fa() {
// 刪除 return
await 1
} fa().then(v => {
console.log(v)
}) // undefined

reject 中斷 async 函式

await 命令後的 Promise 物件如果變成 reject 狀態,則 reject 的引數會被 catch 方法回撥函式接收。就像這樣:

async function foo() {
await Promise.reject(1) // {1}
} foo().then(v => {
console.log(v)
}).catch(v => {
console.log(`catch, ${v}`)
}) // catch, 1

請注意,await 語句(行{1})前面沒有 return 語句,但是 reject() 方法的引數依然傳入了 catch 方法的回撥函式中,這點與 resolve 狀態不相同。

只要一個 await 語句後面的 Promise 變成 reject,那麼整個 async 函式都會中斷。請看示例:

async function foo() {
await Promise.reject(1) await new Promise((resolve, reject) => {
console.log(2)
resolve()
})
return 3
} foo().then(v => {
console.log(v)
}).catch(v => {
console.log(`catch, ${v}`)
}) // catch, 1

由於第一個 await 後面的 Promise 變成 reject,整個 async 函式就中斷執行。

如果我們希望前一個非同步操作失敗,也不中斷後面的非同步操作,可以這麼寫:

try{
await Promise.reject(1)
}catch(e){ } // 亦或者
// 在 Promise 一文中提到拒絕處理程式能恢復整條鏈的執行
await Promise.reject(1).catch(() => {})
...

如果 await 後面的非同步操作出錯,那麼等同於 async 函式返回的 Promise 物件被 reject。就像這樣:

async function foo() {
await new Promise((resolve, reject) => {
throw new Error('fail')
})
return 3
} foo().then(v => {
console.log(v)
}).catch(v => {
console.log(`catch, ${v}`)
}) // catch, Error: fail

防止出錯的方法也是將其放在 try ... catch 方法中。下面例子使用 try...catch 實現多次嘗試:

function request(v){
return new Promise((resolve, reject) => {
if(v == 2){
console.log(`resolve${v}`)
resolve(v)
}else{
console.log(`fail${v}`)
throw new Error('fail')
}
})
} async function foo() {
for(let i = 0; i < 5; i++){
try{
await request(i)
break
}catch(e){} // {1}
}
return 'end'
} foo().then(v => {
console.log(v)
}).catch(v => {
console.log(`catch, ${v}`)
}) // fail0 fail1 resolve2 end

這段程式碼,如果 await 操作成功,則會使用 break 語句退出迴圈;如果失敗,則會被 catch(行{1}) 捕獲,然後進入下一輪迴圈。

await 與並行

下面的程式碼,會依次輸出 1 和 2,屬於序列。

function createPromise(val, time = 1000){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(val)
resolve(val)
}, time)
})
} async function foo() {
let a = await createPromise(1)
let b = await createPromise(2)
} foo() // 1 2

如果多個非同步操作不存在繼發關係,最好讓它們同時觸發。將 foo() 方法改為下面任一方式:

// 方式一
async function foo() {
let p1 = createPromise(1)
let p2 = createPromise(2)
// 至此,兩個非同步操作都已經發出
await p1
await p2
} // 方式二
async function foo() {
let [p1, p2] = await Promise.all([createPromise(1), createPromise(2)])
}

再次執行,只需要 1 秒就會同時輸出 1 2。

async 函式中的 await

await 關鍵字只能用在 async 函式中。請看示例:

async function fa(){
let arr = [1, 2, 3] arr.forEach(v => {
await v
})
}
// SyntaxError: await is only valid in async function

這段程式碼將報語法錯誤。

如果將 forEach 方法的引數改為 async 函式,就像這樣:

function createPromise(val, time = 1000){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(val)
resolve(val)
}, time)
})
} async function fa(){
let arr = [1, 2, 3]
// 改為 async 函式
arr.forEach(async v => {
await createPromise(v)
})
} fa() // 1 2 3

等待 1 秒後同時輸出 1 2 3。因為這 3 個非同步操作是併發執行。

如果希望多個請求併發執行,也可以使用 Promise.all 方法。就像這樣:

// 替換 fa() 方法即可
async function fa(){
let arr = [1, 2, 3]
let promises = arr.map(v => createPromise(v))
let results = await Promise.all(promises)
console.log(results)
}

而如果需要繼發,可以採用 for 迴圈:

// 替換 fa() 方法即可
async function fa(){
let arr = [1, 2, 3]
// 將 forEach 改為 for 迴圈
for(let i = 0; i < arr.length; i++){
await createPromise(arr[i])
}
}

每過一秒,會依次輸出 1 2 3。

其他章節請看:

es6 快速入門 系列