理解 async/await

饅頭君發表於2019-02-02

什麼是async?


現在面對日常工作時,總避免不了面對非同步操作帶來的一些麻煩。在時代演變的過程中,處理非同步的方法有許多種:回撥函式、Promise 鏈式語法、Generator 函式到現在比較流行的 async 函式。那什麼是 async 呢?

async 函式是 Generator 函式的語法糖。使用 async 關鍵字代替 Generator 函式的星號 *await 關鍵字代替 yield。相較於Generator函式,async函式改進了以下四點:

  • 內建執行器 Generator 函式的執行必須靠執行器,所以才有了 co 模組,而 async 函式自帶執行器。
  • 更好的語義 asyncawait,比起 *yield,語義更清楚。async 表示函式裡有非同步操作,await 表示緊跟在後面的表示式需要等待結果。
  • 更廣的適用性 co 模組約定,yield 命令後面只能是Thunk 函式或Promise 物件,而async 函式的await 命令後面,可以是Promise物件和原始型別的值。
  • 返回值是 Promise async 函式的返回值是Promise物件,這比Generator 函式的返回值是 Iterator物件方便多了。你可以用 then 方法指定下一步的操作。

async 用法


關於async的用法,先看一個簡單的小例子:

function getProvinces () {
    return new Promise(resolve => {
        setTimeout(resolve, 1000)
    })
}
async function asyncFn () {
    await getProvinces()
    console.log('hello async')
}
複製程式碼

上面程式碼先定義了一個獲取省份資料的getProvinces函式,其中用setTimeout模擬資料請求的非同步操作。當我們在asyncFn 函式前面使用關鍵字 async 就表明該函式記憶體在非同步操作。當遇到 await 關鍵字時,會等待非同步操作完成後再接著執行接下去的程式碼。所以程式碼的執行結果為等待1000毫秒之後才會在控制檯中列印出 'hello async'。

瞭解了 async 的基本用法,接下來理解一下 async 的運作:

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

async function asyncFn2 () {
    throw new Error('error')
}
asyncFn2().then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})
// 'error'
複製程式碼

async 會返回一個Promise物件,當沒發生錯誤時 return 的值會成為 then 方法回撥函式的引數。而當丟擲錯誤時,會導致Promise物件變為 reject 狀態,丟擲的錯誤也會成為 catch 方法回撥函式的引數。

async function asyncFn3 () {
    return await Promise.resolve('hello async')
}
asyncFn3().then(res => console.log(res))
// 'hello async'

async function asyncFn4 () {
    return await 123
}
asyncFn3().then(res => console.log(res))
// 123
複製程式碼

await(async wait)關鍵字後面如果是一個Promise物件,則會返回該Promise的結果。如果不是,也會當成立即執行resolve,將值返回。

async 函式當中存在多個 await 的函式時,我們不得不考慮某個Promise狀態變為 reject 的情況,因為只要內部有函式狀態改變為 reject 時,接下去的函式將不再執行,async 函式的狀態也將變更為 reject

async function asyncFn5 () {
    await Promise.reject('error')
    return await Promise.resolve('hello async') // 不會執行
}
asyncFn5().then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})
複製程式碼

為了能夠正確的執行程式碼,應該對 await 進行錯誤處理,基本的錯誤處理方式有兩類:

async function asyncFn6 () {
    try {
        await Promise.reject('error')
    } catch (err) {
        console.log(err)
    }
    return await Promise.resolve('hello async')
}
// 將可能發生錯誤的函式使用try...catch進行處理

async function asyncFn7 () {
    await Promise.reject('error').catch(err => console.log(err))
    return await Promise.resolve('hello async')
}
// 將可能變為reject狀態的Promise物件後面跟上一個catch方法,以處理之前發生的錯誤
複製程式碼

理解了 async/await 的基本用法,接下來用一個工作中經常會遇到的情景作為例子,感受一下 async/await 的魔力。

假設我們現在要獲取一個地級市擁有多少個轄區,我們現在得先呼叫獲取當地省份的介面,從中拿到省份id才能夠呼叫獲取地級市的介面,拿到對應地級市的id才能獲取最終的結果。

function getProvinces () {
    ...
    return new Promise(resolve => {
        resolve(provinceId)
    }
}
function getCitys (provinceId) {
    ...
    return new Promise(resolve => {
        resolve(cityId)
    }
}
function getArea (cityId) {
    ...
    return new Promise(resolve => {
        resolve(areaData)
    }
}
複製程式碼

如果用 Promise 實現是這樣:

getProvinces().then(provinceId => getCitys(provinceId)).then(cityId => getArea(cityId)
複製程式碼

再來看看用 async/await 實現方式:

async getData () {
    const provinceId = await getProvinces()
    const cityId = await getCitys(provinceId)
    return await getArea(cityId)
}

getData()
複製程式碼

雖然兩種方法都能夠達到我們最終的目的,但是在依賴關係更加複雜的情況下,使用 Promise 的方式會使得鏈式非常的長,並且相比使用 async/await 程式碼閱讀性會更低。

async執行過程

在工作中 async 的應用情況更加多種,因為其看似同步的處理非同步操作,解決了不斷回撥的問題,增加了程式碼的可閱讀性。 async 雖然看似同步操作,但是它式非阻塞的,接下來將 asyncPromisesetTimeout 結合,用一個小例子加深對 async 的理解:

async function asyncFn1 () {
    console.log('asyncFn1 start')
    await asyncFn2()
    console.log('async1 end')
}

async function asyncFn2 () {
    console.log('asyncFn2')
}

console.log('script start')

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

asyncFn1()

new Promise((resolve) => {
    console.log('Promise')
    resolve()
}).then(() => {
    console.log('Promise.then')
})
console.log('script end')
複製程式碼

上面的程式碼,執行過程中會列印出8條語句,請大家先花一些時間思考一下執行順序。

最終在控制檯中的列印結果為:

script start
asyncFn1 start
asyncFn2
Promise
script end
Promise.then
async1 end
setTimeout
複製程式碼

應該有許多人的答案都是正確的,假如你的答案與正確答案有些許偏差,也沒關係,通過這道題你能更深入的理解非同步執行的問題,這段程式碼的執行順序其實是這樣的:

  1. 定義非同步的asyncFn1函式
  2. 定義非同步的asyncFn2函式
  3. 執行console.log('script start')語句 * 1
  4. 定義一個定時器在0ms後輸出(setTimeout會被加入到macrotasks佇列中,所以執行優先順序比被加入microtasks佇列的低)
  5. 執行asyncFn1函式 :

(1)執行console.log('asyncFn1 start')語句 * 2

(2)遇到await,執行asyncFn2函式 * 3(此時讓出執行緒,跳出asyncFn1函式,繼續執行同步棧的任務)

  1. 執行Promise語句

(1)執行console.log('Promise')語句 * 4

(2)resolve(),返回一個Promise物件,將這個Promise物件加入到microtasks佇列中

  1. 執行console.log('script end')語句 * 5
  2. 同步棧執行完畢
  3. 回到asyncFn1函式體中,將asyncFn2函式返回的Promise物件加入到microtasks佇列中
  4. 取出microtasks佇列中的任務,列印console.log('Promise.then') * 6
  5. 接著執行asyncFn1函式體中console.log('asyncFn1 end')語句 * 7
  6. 最後執行macrotasks佇列中的任務,執行console.log('setTimeout') * 8

以上是我對async/await知識的一些拙見,寫下這篇文章單純為了鞏固自身的知識,希望也能對讀者有一點點幫助。

本文總結參考自:阮一峰的ESMAScript6入門以及前端er,你真的會用async嗎?

相關文章