什麼是async?
現在面對日常工作時,總避免不了面對非同步操作帶來的一些麻煩。在時代演變的過程中,處理非同步的方法有許多種:回撥函式、Promise
鏈式語法、Generator
函式到現在比較流行的 async
函式。那什麼是 async
呢?
async
函式是 Generator
函式的語法糖。使用 async
關鍵字代替 Generator
函式的星號 *
,await
關鍵字代替 yield
。相較於Generator函式,async函式改進了以下四點:
- 內建執行器
Generator
函式的執行必須靠執行器,所以才有了co
模組,而async
函式自帶執行器。 - 更好的語義
async
和await
,比起*
和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
雖然看似同步操作,但是它式非阻塞的,接下來將 async
、 Promise
和 setTimeout
結合,用一個小例子加深對 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
複製程式碼
應該有許多人的答案都是正確的,假如你的答案與正確答案有些許偏差,也沒關係,通過這道題你能更深入的理解非同步執行的問題,這段程式碼的執行順序其實是這樣的:
- 定義非同步的asyncFn1函式
- 定義非同步的asyncFn2函式
- 執行console.log('script start')語句 * 1
- 定義一個定時器在0ms後輸出(setTimeout會被加入到macrotasks佇列中,所以執行優先順序比被加入microtasks佇列的低)
- 執行asyncFn1函式 :
(1)執行console.log('asyncFn1 start')語句 * 2
(2)遇到await,執行asyncFn2函式 * 3(此時讓出執行緒,跳出asyncFn1函式,繼續執行同步棧的任務)
- 執行Promise語句
(1)執行console.log('Promise')語句 * 4
(2)resolve(),返回一個Promise物件,將這個Promise物件加入到microtasks佇列中
- 執行console.log('script end')語句 * 5
- 同步棧執行完畢
- 回到asyncFn1函式體中,將asyncFn2函式返回的Promise物件加入到microtasks佇列中
- 取出microtasks佇列中的任務,列印console.log('Promise.then') * 6
- 接著執行asyncFn1函式體中console.log('asyncFn1 end')語句 * 7
- 最後執行macrotasks佇列中的任務,執行console.log('setTimeout') * 8
以上是我對async/await知識的一些拙見,寫下這篇文章單純為了鞏固自身的知識,希望也能對讀者有一點點幫助。
本文總結參考自:阮一峰的ESMAScript6入門以及前端er,你真的會用async嗎?