JavaScript:async/await的基礎用法

風靈使發表於2018-06-26

相對於回撥函式來說,Promise是一種相對優雅的選擇。那麼有沒有更好的方案呢?答案就是async/await
優勢主要體現在,級聯呼叫,也就是幾個呼叫依次發生的場景。
async/await。被稱為到目前最優雅的非同步過程解決方案,不知道你是否認同,反正我是信了。

相對於Promiseasync/await有什麼優點?

比較場景: 級聯呼叫,也就是幾個呼叫依次發生的場景

  • Promise主要用then函式的鏈式呼叫,一直點點點,是一種從左向右的橫向寫法。
    async/await從上到下,順序執行,就像寫同步程式碼一樣。這更符合人編寫程式碼的習慣
  • Promisethen函式只能傳遞一個引數,雖然可以通過包裝成物件,但是這會導致傳遞冗餘資訊,頻繁的解析又重新組合引數,比較麻煩。
    async/await沒有這個限制,就當做普通的區域性變數來處理好了,用let或者const定義的塊級變數,想怎麼用就怎麼用,想定義幾個就定義幾個,完全沒有限制,也沒有冗餘的工作。
  • Promise在使用的時候最好將同步程式碼和非同步程式碼放在不同的then節點中,這樣結構更加清晰。
    async/await整個書寫習慣都是同步的,不需要糾結同步和非同步的區別。當然,非同步過程需要包裝成一個Promise物件,放在await關鍵字後面,這點還是要牢記的。
  • Promise是根據函數語言程式設計的正規化,對非同步過程進行了一層封裝。
    async/await是基於協程的機制,是真正的“儲存上下文,控制權切換 … … 控制權恢復,取回上下文”這種機制,是對非同步過程更精確的一種描述。

程式、執行緒和協程的理解
上面的文章很好地解釋了這幾個概念的區別。
如果不糾結細節,可以簡單地認為:程式 > 執行緒 > 協程;
協程可以獨立完成一些與介面無關的工作,不會阻塞主執行緒渲染介面,也就是不會卡。
協程,雖然小一點,不過能完成我們程式設計師交給的任務。而且我們可以自由控制執行和阻塞狀態,不需要求助於高大上的系統排程,這才是重點。

  • async/await是基於Promise的,是進一步的一種優化。不過再寫程式碼的時候,Promise本身的API出現得很少,很接近同步程式碼的寫法。

await關鍵字使用時有哪些注意點?

  • 只能放在async函式內部使用,不能放在普通函式裡面,否則會報錯。
  • 後面放Promise物件,在Pending狀態時,相應的協程會交出控制權,進入等待狀態。這個是本質。
  • awaitasync wait的意思,wait的是resolve(data)訊息,並把資料data返回。比如,下面程式碼中,當Promise物件由Pending變為Resolved的時候,變數a就等於data;然後再順序執行下面的語句console.log(a);

這真的是等待,真的是順序執行,表現和同步程式碼幾乎一模一樣。

const a = await new Promise((resolve, reject) => {
    // async process ...
    return resolve(data);
});
console.log(a);
  • await後面也可以跟同步程式碼,不過系統會自動轉化成一個Promise物件。

比如

    const a = await 'hello world';

其實就相當於

    const a = await Promise.resolve('hello world');

這跟同步程式碼
const a = 'hello world';是一樣的,還不如省點事,去掉這裡的await關鍵字。

  • await只關心非同步過程成功的訊息resolve(data),拿到相應的資料data。至於失敗訊息reject(error),不關心,不處理。

當然對於錯誤訊息的處理,有以下幾種方法供選擇:
(1)讓await後面的Promise物件自己catch
(2)也可以讓外面的async函式返回的Promise物件統一catch
(3)像同步程式碼一樣,放在一個try...catch結構中

async關鍵字使用時有哪些注意點?

  • 有了這個async關鍵字,只是表明裡面可能有非同步過程,裡面可以有await關鍵字。當然,全部是同步程式碼也沒關係。當然,這時候這個async關鍵字就顯得多餘了。不是不能加,而是不應該加。
  • async函式,如果裡面有非同步過程,會等待;
    但是async函式本身會馬上返回,不會阻塞當前執行緒。

可以簡單認為,async函式工作在主執行緒,同步執行,不會阻塞介面渲染。
async函式內部由async關鍵字修飾的非同步過程,工作在相應的協程上,會阻塞等待非同步任務的完成再返回。

async函式的返回值是一個Promise物件,這個是和普通函式本質不同的地方。這也是使用時重點注意的地方
(1)return newPromise();這個符合async函式本意;
(2)return data;這個是同步函式的寫法,這裡是要特別注意的。這個時候,其實就相當於Promise.resolve(data);還是一個Promise物件。
在呼叫async函式的地方通過簡單的=是拿不到這個data的。
那麼怎麼樣拿到這個data呢?
很簡單,返回值是一個Promise物件,用.then(data => { })函式就可以。
(3)如果沒有返回,相當於返回了Promise.resolve(undefined);

  • await是不管非同步過程的reject(error)訊息的,async函式返回的這個Promise物件的catch函式就負責統一抓取內部所有非同步過程的錯誤。
    async函式內部只要有一個非同步過程發生錯誤,整個執行過程就中斷,這個返回的Promise物件的catch就能抓到這個錯誤。
  • async函式執行和普通函式一樣,函式名帶個()就可以了,引數個數隨意,沒有限制;也需要有async關鍵字。
    只是返回值是一個Promise物件,可以用then函式得到返回值,用catch抓去整個流程中發生的錯誤。

基本套路

Step1:用Promise物件包裝非同步過程,這個和Promise的使用一樣。只是引數個數隨意,沒有限制。

function sleep(ms) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('sleep for ' + ms + ' ms');
        }, ms);
    });
}

Step2:定義非同步流程,可以將按照需要定製,就像寫同步程式碼那樣

async function asyncFunction() {
    console.time('asyncFunction total executing:');
    const sleep1 = await sleep(2000);
    console.log('sleep1: ' + sleep1);
    const [sleep2, sleep3, sleep4]= await Promise.all([sleep(2000), sleep(1000), sleep(1500)]);
    console.log('sleep2: ' + sleep2);
    console.log('sleep3: ' + sleep3);
    console.log('sleep4: ' + sleep4);
    const sleepRace = await Promise.race([sleep(3000), sleep(1000), sleep(1000)]);
    console.log('sleep race: ' + sleepRace);
    console.timeEnd('asyncFunction total executing:');

    return 'asyncFunction done.'  // 這個可以不返回,這裡只是做個標記,為了顯示流程
}

Step3:像普通函式呼叫async函式,在then函式中獲取整個流程的返回資訊,在catch函式統一處理出錯資訊

asyncFunction().then(data => {
    console.log(data);       // asyncFunction return 的內容在這裡獲取
}).catch(error => {
    console.log(error);      // asyncFunction 的錯誤統一在這裡抓取
});


console.log('after asyncFunction code executing....'); // 這個代表asyncFunction函式後的程式碼,
                                                       // 顯示asyncFunction本身會立即返回,不會阻塞主執行緒

流程解析

上面的程式碼執行之後,輸出的log如下,顯示了程式碼執行流程

after asyncFunction code executing....
sleep1: sleep for 2000 ms
sleep2: sleep for 2000 ms
sleep3: sleep for 1000 ms
sleep4: sleep for 1500 ms
sleep race: sleep for 1000 ms
asyncFunction total executing:: 5006.276123046875ms
asyncFunction done.
  1. after asyncFunction code executing....程式碼位置在async函式asyncFunction()呼叫之後,反而先輸出。這說明async函式asyncFunction()呼叫之後會馬上返回,不會阻塞主執行緒。
  2. sleep1: sleep for 2000 ms這是第一個await之後的第一個非同步過程,最先執行,也最先完成,說明後面的程式碼,不論是同步和非同步,都在等他執行完畢。
  3. sleep2 ~ sleep4這是第二個await之後的Promise.all()非同步過程。這是“比慢模式”,三個sleep都完成後,再執行下面的程式碼,耗時最長的是2000ms
  4. sleep race: sleep for 1000 ms這是第三個await之後的Promise.race()非同步過程。這是“比快模式”,耗時最短sleep都完成後,就執行下面的程式碼。耗時最短的是1000ms
  5. asyncFunction total executing::5006.276123046875ms這是最後的統計總共執行時間程式碼。三個await之後的非同步過程之和1000(獨立的) + 2000(Promise.all) + 1000(Promise.race) = 5000ms
    這個和統計出來的5006.276123046875ms非常接近。說明上面的非同步過程,和同步程式碼執行過程一致,協程真的是在等待非同步過程執行完畢。
  6. asyncFunction done.這個是async函式返回的資訊,在執行時的then函式中獲得,說明整個流程完畢之後引數傳遞的過程。

log

異常處理

  • async標註過的函式,返回一個Promise物件,採用.then().catch()的方式來進行異常處理,是非常自然的方法,也推薦這麼做。就像上面的step3那樣做。
  • 另外一種方法,就是對於非同步過程採用await關鍵字,採用同步的try{} catch(){}的方式來進行異常處理。
  • 這裡要注意的是await關鍵字只能用在async標註的函式中,所以,原來的函式,不管以前是同步的還是非同步的,都要加上async關鍵字,比如componentDidMount()就要變為async componentDidMount()才可以在內部使用await關鍵字,不過功能上沒有任何影響。
  • 另外,採用同步的try{} catch(){}的方式,可以把同步,非同步程式碼都可以放在裡面,有錯誤都能抓到,比如null.length這種,也能抓到。
async componentDidMount() { // 這是React Native的回撥函式,加個async關鍵字,沒有任何影響,但是可以用await關鍵字
    // 將非同步和同步的程式碼放在一個try..catch中,異常都能抓到
    try {
        let array = null;
        let data = await asyncFunction();  // 這裡用await關鍵字,就能拿到結果值;否則,沒有await的話,只能拿到Promise物件
        if (array.length > 0) {  // 這裡會丟擲異常,下面的catch也能抓到
            array.push(data);
        }
    } catch (error) {
        alert(JSON.stringify(error))
    }
}

這裡模擬的是網路過程。一般情況,array是一個陣列,用if (array.length > 0)判斷一下長度,有值再處理,沒有問題。但是,一旦網路出問題,array就是一個null,平時工作很好的if (array.length > 0)判斷就會拋異常,JS程式碼就中斷,停止工作,會帶來意想不到的問題。

這裡加了一個try..catch結構,這種異常就能捕獲,(這是同步程式碼中的異常,不能用.then().catch()抓到),根據異常資訊,一般是null沒有length屬性,方便定位問題。這裡的話用if (array && (array.length > 0))就會安全一點。

參考文章

本文只是介紹了async/await一種基礎的用法。一個例子,將三種Promise使用中常用的場景模式都包括進去了,並且程式碼風格和同步程式碼非常相似。相比之下async/await這套非同步程式碼程式設計方式確實比較優雅。

理解 JavaScript 的 async/await

深入理解ES7的async/await

async 函式

相關文章