前端er,你真的會用 async 嗎?

async 異步函數 不完全使用攻略

前言

現在已經到 8012 年的尾聲了,前端各方面的技術發展也層出不窮,VueConf TO 2018 大會 也發佈了 Vue 3.0的計劃。而在我們(我)的日常中也經常用 Vue 來編寫一些項目。那麼,就少不了 ES6 的登場了。那麼話說回來,你真的會用 ES6 的 async 異步函數嗎?

1、async 介紹

先上 MDN 介紹:https://developer.mozilla.org...

async function 用於聲明 一個 返回 AsyncFunction 對象的異步函數。異步函數是值通過事件循環異步執行的函數,它會通過一個隱式的 Promise 返回其結果。如果你的代碼使用了異步函數,它的語法和結構更像是標準的同步函數

人工翻譯:async 關鍵字是用於表示一個函數裏面有異步操作的含義。它通過返回一個 Promise 對象來返回結果它的最大的特點是:通過 async / await 將異步的操作,但是寫法和結構卻是和我們平時寫的(同步代碼)是一樣

2、示範

// 一般我們會把所有請求方法都定義在一個文件裏,這裏定義一個方法來模擬我們的日常請求
function fetch() {
    axios.get('/user?ID=12345')
      .then(function (response) {
        console.log(response);
      })
      .catch(function (error) {
        console.log(error);
      });
};
// 然後在需要它的地方調用它
async function getUserInfo() {
    const info = await fetch();

    return info;
}
getUserInfo().then(info => console.log(info));

我們可以看到,整個過程非常直觀和清晰,語句語義非常明確,整個異步操作看起來就像是同步一樣。如果看完上面的流程沒有問題的話,那我們接下來繼續深入的瞭解一下。

3、async Promise setTimeout(定時器) 的結合使用情況

接下來給大家演示一道題目,這道題是我當時面某條的面試題,估計和多人也見過,這道題非常經典而且使用場景頁非常多,研究意義非常大,那麼我在這裏就給大家分享一下。

求下面的輸出結果:
async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}
console.log('script start')
setTimeout(function(){
    console.log('setTimeout')
},0)  
async1();
new Promise(function(resolve){
    console.log('promise1')
    resolve();
}).then(function(){
    console.log('promise2')
})
console.log('script end')

這裏一共有 8 條 log 語句,先別複製到控制檯上,大家給20秒鐘的時間默唸一下輸出的順序。

1..2.. .. .. 20

我先給上正確的答案:

script start
async1 start
async2
promise1
script end
promise2
async1 end
setTimeout

如果你的答案和上面的正確答案有所偏差,那麼說明你對 async / await 的理解還是不夠深刻,希望你閱讀完我的這篇文章之後可以直面各種同步異步問題了(嘻嘻,這還不點個贊嘛)

我們再來回顧一下 MDN 對 async / await 的描述:

當調用一個 async 函數時,會返回一個 Promise 對象。當這個 async 函數返回一個值時,Promise 的 resolve 方法會負責傳遞這個值;當 async 函數拋出異常時,Promise 的 reject 方法也會傳遞這個異常值。

async 函數中可能會有 await 表達式,這會使 async 函數暫停執行,等待 Promise 的結果出來,然後恢復async函數的執行並返回解析值(resolved)。

async/await的用途是簡化使用 promises 異步調用的操作,並對一組 Promises執行某些操作。正如Promises類似於結構化回調,async/await類似於組合生成器和 promises。

await

await 操作符用於等待一個Promise 對象。它只能在異步函數 async function 中使用。

[return_value] = await expression;

await 表達式會暫停當前 async function 的執行,等待 Promise 處理完成。若 Promise 正常處理(fulfilled),其回調的resolve函數參數作爲 await 表達式的值,繼續執行 async function

若 Promise 處理異常(rejected),await 表達式會把 Promise 的異常原因拋出。

另外,如果 await 操作符後的表達式的值不是一個 Promise,則返回該值本身。

其中非常重要的一句是:遇到 await 表達式時,會讓 async 函數 暫停執行,等到 await 後面的語句(Promise)狀態發生改變(resolved或者rejected)之後,再恢復 async 函數的執行(再之後 await 下面的語句),並返回解析值(Promise的值)

這麼多 Promise 相關的內容是因爲async / await 是建立在 Promise 的基礎上的呀~~

然後再來回頭看我們的題目,會發現,有點不對勁啊

async1 end
promise2

那是因爲還有一個Promise.resolve 的點沒有考慮,這也是我中招的點

4、分析過程

  1. 定義一個異步函數 async1
  2. 定義一個異步函數 async2
  3. 打印 ‘script start’ // *1
  4. 定義一個定時器(宏任務,優先級低於微任務),在0ms 之後輸出
  5. 執行異步函數 async1

    1. 打印 'async1 start' // *2
    2. 遇到await 表達式,執行 await 後面的 async2

      1. 打印 'async2' // *3
    3. 返回一個 Promise,跳出 async1 函數體
  6. 執行 new Promise 裏的語句

    1. 打印 ‘promise1‘ // *4
    2. resolve() , 返回一個 Promise 對象,把這個 Promise 壓進隊列裏
  7. 打印 ’script end' // *5
  8. 同步棧執行完畢
  9. 回到 async1 的函數體,async2 函數沒有返回 Promise,所以把要等async2 的值 resolve,把 Promise 壓進隊列
  10. 執行 new Promise 後面的 .then,打印 ’promise2‘ // *6
  11. 回到 async1 的函數體,await 返回 Promise.resolve() ,然後打印後面的 ’async1 end‘ // *7
  12. 最後執行定時器(宏任務) setTimeout,打印 ’setTimeout‘ // *8

我對這段代碼的過程分析大致如上(如果有什麼理解不對的地方請指出),這裏有很關鍵而且是大家容易理解錯誤的點是:很多人以爲 await 會一直等待後面的表達式執行完之後纔會執行後續代碼,實際上 await 是會先執行後面的表達式,然後返回一個Promise,接着就跳出整個 async 函數來執行後面的代碼,也就是說執行到 await 的時候,會有一個 讓出線程 的操作。等後面的同步站執行完了之後,又會回到 async 函數中等待 await 表達式的返回值,如果不是一個 Promise 對象,則會有一個期待它 resolve 成爲一個 Promise對象的過程,然後繼續執行 async 函數後面的代碼,直到是一個 Promise 對象,則把這個 Promise 對象放入 Promise 隊列裏。

所以說 ,’async1 end' 和‘promise2‘ 這個不注意就會出錯的難點就是這樣

那麼現在,我們是不是大致上對async / await 理解了呢,我們來改一下這道題再來看看,把 async2 改造一下

async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
function async2(){ // 去掉了 async 關鍵字
    console.log('async2');
}
console.log('script start')
setTimeout(function(){
    console.log('setTimeout')
},0)  
async1();
new Promise(function(resolve){
    console.log('promise1')
    resolve();
}).then(function(){
    console.log('promise2')
})
console.log('script end')

這次大家能做對了嗎~

5、日常常用示例

上面寫了那麼多,只是爲了方便大家對於異步函數的理解,

下面給一些我們日常開發中使用異步函數的例子。一般來說,我們有一個業務需要分不完成,每個步驟都是異步的,並且嚴重依賴於上一步的執行結果,稍有不慎就會進入回調地獄(callback hell)了,這種情況下,我們可以用 async / await 來完成

// 比如在這裏場景,我們提交數據的時候先判定用戶是否有這個權限,然後再進行下一步動作
async function submitData(data) {
    const res = await getAuth(); // 獲取授權狀態
    if (res....) {
        const data = await submit(data);
    }
    toast(data.message);
}

這樣就可以保證兩個操作的先後順序

或者是在 Vue 中,一些初始化的操作

async created() {
    const res = await this.init(); // 獲取列表等操作
    const list = await this.getPage(); // 分頁請求等
}

但是在使用過程中,我們會發現剛從回調地獄中解救,然後就陷入 async / await 地獄的誕生

舉一個例子:

async created() {
    const userInfo = await this.getUserInfo(); // 獲取用戶數據
    const list = await this.getNewsList(); // 獲取文章數據
}

表面上看,這段語法是正確的,但並不是一個優秀實現,因爲它把兩個沒有先後順序的一部操作強行變成同步操作了,因爲這裏的代碼是一行接着一行執行的,想一下,我們沒有必要在獲取用戶數據之後纔去獲取文章數據,它們的工作是可以同時進行的

這裏給出一些常用的併發執行的實例

async created() {
    const userInfo = this.getUserInfo(); // 它們都會返回 Promise 對象
    const list = this.getNewsList();
    await userInfo;
    await list;
    // ...do something
}
// 如果有很多請求的情況下可以使用 Promise.all
async created() {
    Promise.all([this.getUserInfo(), this.getNewsList()]).then(()=> {
        // ...do something
    });
}

5、圖例

async 導圖

6、小結

1、異步的終極解決方案

2、看起來像同步的異步操作

3、便捷的捕獲錯誤和調試

4、支持併發執行

5、要知道避免 async / await 地獄

7、寫在最後

好了,關於async 異步函數的不完全指南就說到這裏了,上面所提及的內容,可能也就比較淺顯的內容。而且有時候,建議大家熟練使用它,在日常開發中多使用多總結纔會有沉澱的效果,都是要靠自己多練,才能熟悉使用,熟能生巧!
最後,如果大家覺得我有哪裏寫錯了,寫得不好,有其它什麼建議(誇獎),非常歡迎大家補充。希望能讓大家交流意見,相互學習,一起進步!
我是一名 19 的應屆新人,以上就是今天的分享,新手上路中,後續不定期周更(或者是月更哈哈),我會努力讓自己變得更優秀、寫出更好的文章,文章中有不對之處,煩請各位大神斧正。如果你覺得這篇文章對你有所幫助,請記得點贊或者品論留言哦~。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章