理解 async/await

什麼是async?


如今面對平常工做時,總避免不了面對異步操做帶來的一些麻煩。在時代演變的過程當中,處理異步的方法有許多種:回調函數、Promise 鏈式語法、Generator 函數到如今比較流行的 async 函數。那什麼是 async 呢?javascript

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的用法,先看一個簡單的小例子:java

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'。es6

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

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

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 進行錯誤處理,基本的錯誤處理方式有兩類:ui

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 的魔力。spa

假設咱們如今要獲取一個地級市擁有多少個轄區,咱們如今得先調用獲取當地省份的接口,從中拿到省份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嗎?

相關文章
相關標籤/搜索