iT邦幫忙

2024 iThome 鐵人賽

DAY 19
0
JavaScript

Don't make JavaScript Just Surpise系列 第 19

async 和 await 關鍵字

  • 分享至 

  • xImage
  •  

在異步的處理上,我們從最初的 callback(副作用:回呼地獄),到後來的 Promise(解決回呼地獄,鏈式呼叫),以及上篇的 Generator + Promise(讓異步呼叫處理上能更接近同步風格)。

直到了 ES 2017,又推出了新的處理異步的方法:asyncawait
Generator + Promise 儘管語法使用上已經比前幾個版本優化了不少,但對使用者來說需要理解的內容較多較複雜,asyncawait 這組語法更簡潔,甚至你不需去理解背後的機制,只需理解這兩個語法糖如何使用,就能夠用近似同步風格的程式碼來處理異步操作。

這兩個語法也是基於 Promise 衍生的。

  1. async
    寫於一個異步函式的定義之前。被加上 async 關鍵字的函式,即使沒有任何返回值,也會預設返回一個 Promise 物件。

    如果該函式有返回值,則會將返回值存在 Promise 物件中;如果拋出例外,則 Promise 物件的狀態會是 reject

    async function foo(input) {
        if(typeof input === "undefined")
        return 'bar!';
    else
        throw Error("Don't put anything in the ()!");
    }
    
    console.log(foo().toString());//"[object Promise]"
    console.log(foo() instanceof Promise);//true
    foo().then(x=>{console.log(x)});//"bar!"
    foo("garbage")
    .then(x=>{console.log(`Fulfill:${x}`)})
    .catch(x=>{console.log(`Reject:${x}`)});//"Reject:Error: Don't put anything in the ()!"
    

    如上所說,即使裡面根本沒有特定的異步操作,加上 async 後就會使該函式回傳一個 Promise 物件,這個例子也展示了上面提到的回傳值與例外的情況。

  2. await
    只能使用於 async 函式內部,用於等待一個 Promise 被解決(Settle),並獲取返回的值。
    在被解決前,程式碼會在該處進行等待直至得到返回值。

    async function cookEgg(waterAmount=0) {
        console.log("開始煮水");
        try {
            await boilWater(waterAmount);
            console.log("可以來煮蛋囉!");
        } catch (error) {
            console.log("發生錯誤:", error);
        }
    }
    
    function boilWater(waterAmount) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
            if (waterAmount < 1) {
                reject("水量太少");
            } else {
                console.log("水煮開了");
                resolve();
            }
            }, 1000);
        });
    }
    
    cookEgg(2);
    

    boilWater 是一個異步操作的函式,回傳了一個 Promise 物件,在外面調用的時候使用了 await 關鍵字來等待煮水的結果,確定煮完才能繼續往下一步前進。
    也因為了 await,所以 cookEgg 函式需要加上 async 關鍵字。

    await 僅能使用於 async 內部的原因是因為 async 語法提供了異步的上下文,這是一般函式中沒有的(一般函式由上而下執行,,無法暫停,無法等待異步),透過 async 提供的上下文,得以實現等待 await 的行為:暫停與恢復,同時 async 也能明確標示這個函式是一個異步函式,避免同步異步混用,而不易辨別流程順序的情況。

    與生成器(Generator)相比,不只語法變得簡潔,錯誤處理也更容易編寫。
    生成器中的 try catch 必須寫在生成器裡面,若外部發生錯誤,則須對返回的生成器物件使用 .throw() 來觸發例外處理。

    asyncawait 則可以在 async 函式裡面直接用 try catch 捕捉 await 行執行函式回傳的 Promise 物件為拒絕(reject)時的情形,不再需要在自己異步函式內外雙邊傳遞。

同時發出異步請求

因為 await 會暫停並等待異步函式的回傳,所以當你需要一次呼叫多個異步的時候你沒辦法寫成這樣:

async function foo(){
    await bar1();
    await bar2();
}

這樣的執行順序會變成等待 bar1() 執行完後才接著執行 bar2()
為了處理這個情境,Promise 其實還有個語法可以使用:Promise.all
Promise.all 會接受一個帶有多個 Promise 物件的可迭代物件(如陣列),返回一個單一的 Promise 物件。

async function foo(){
    const [bar1Result, bar2Result] = await Promise.all([bar1(), bar2()]);
}

被回傳的 Promise 物件會在傳入的所有 Promise 物件被 resolve 時回傳 resolve,值會是一個陣列,對應到傳入可迭代物件的各個 Promise 物件。
當然,因為回傳的是一個 Promise 物件,能和 async await 一起使用。

async function foo() {
		try{
      const promise1 = new Promise(resolve => setTimeout(() => resolve("bar1"), 100));
      const promise2 = new Promise(resolve => setTimeout(() => resolve("bar2"), 200));
      const promise3 = new Promise(resolve => setTimeout(() => resolve("bar3"), 50));

      const results = await Promise.all([promise1, promise2, promise3]);
      console.log(results);//["bar1", "bar2", "bar3"]
    }
    catch(error){
	    console.log(error);
    }  
}
foo();

另外,不管 Promise 的完成順序,Promise.all 的回傳是基於呼叫的順序,像上面的例子,理論上完成順序是 3,1,2,但 Promise.all 收到的仍是 1,2,3。

要注意的是,如果其中任一個 Promise 收到 reject,會立刻終止這次的 Promise.all,並回傳該收到 reject 的值。甚至比較早完成的 Promise 的回傳值也都不會顯示,僅會顯示該次 reject 的值。

async function foo() {
		try{
      const promise1 = new Promise(resolve => setTimeout(() => resolve("bar1"), 100));
      const promise2 = new Promise((resolve, reject) => setTimeout(() => reject("fail"), 200));
      const promise3 = new Promise(resolve => setTimeout(() => resolve("bar3"), 50));

      const results = await Promise.all([promise1, promise2, promise3]);
      console.log(results);
    }
    catch(error){
	    console.log(error);
    }  
}
foo();

聽起來是不是有點危險?
沒關係,時間的力量是偉大的,Promise.all 都已經是 ES6 的語法了 -- 在 ES 2020,引入了另一個方法 Promise.allSettled

這個方法使用上和 Promise.all 一模一樣,只差在回傳的邏輯。
這個方法無論中間哪個 Promisereject了,也不會影響對其他 Promise 的等待,回傳的時機點是所有的 Promise 都被 settle 後。

即使中間有 rejectPromise.allSettled 本身的回傳依然是一個 resolvePromise

async function foo() {
	try{
        const promise1 = new Promise(resolve => setTimeout(() => resolve("bar1"), 100));
        const promise2 = new Promise((resolve, reject) => setTimeout(() => reject("fail"), 200));
        const promise3 = new Promise(resolve => setTimeout(() => resolve("bar3"), 50));
        const results = await Promise.allSettled([promise1, promise2, promise3]);
        console.log("In try", results);
        // "In try", 
        // [{
        //     status: "fulfilled",
        //     value: "bar1"
        //     },{
        //     reason: "fail",
        //     status: "rejected"
        //     }, {
        //     status: "fulfilled",
        //     value: "bar3"
        // }]
    }
    catch(error){
	    console.log("In catch", error);
    }  
}
foo();

可以注意到他的回傳結構再稍微複雜一點,針對狀態是 rejectPromise,是一個 {reason: status:} 的物件,針對狀態是 resolvePromsie,則是 {value:,status:} 的物件。
可以用 status 屬性來辨別是否該 Promise 是成功的。


異步相關的主題到此告一個段落,目前異步的主要處理方式多為使用 async await 來做流程控制,優點是語法更為簡潔,容易處理例外。

從前面幾篇到這裡callback -> Promsie -> Promise + Generator -> async + await,逐一介紹各個概念後相信會更清楚自己用的語法的特點與限制,寫起來會更有信心。


上一篇
生成器(Generator)
下一篇
陣列(Array)與相關操作
系列文
Don't make JavaScript Just Surpise31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言