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

Croc_wend發表於2018-12-02

async 非同步函式 不完全使用攻略

前言

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

1、async 介紹

先上 MDN 介紹:developer.mozilla.org/zh-CN/docs/…

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 的應屆新人,以上就是今天的分享,新手上路中,後續不定期周更(或者是月更哈哈),我會努力讓自己變得更優秀、寫出更好的文章,文章中有不對之處,煩請各位大神斧正。如果你覺得這篇文章對你有所幫助,請記得點贊或者品論留言哦~。

相關文章