es6快速入門 系列 - async

彭加李發表於2021-06-30

其他章節請看:

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 快速入門 系列

相關文章