JavaScript學習筆記(一) promise和async/wait

北冥有隻魚發表於2022-11-22

前言

最近我在學習前端相關的知識,沒有意識到的一個問題是我用的Ajax是非同步的,如下面程式碼所示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
<div id = "app">
    <h2>{{ product }} X are in stock .</h2>
    <ul v-for = "product in products" >
        <li>
            <input type="number" v-model.number="product.quantity">
                      {{product.name}} 
            <span v-if = "product.quantity === 0">
                已經售空
            </span>
            <button @click = "product.quantity += 1">
                 新增
            </button>
        </li> 
    </ul>
    <h2> 總庫存 {{ totalProducts}}</h2>
</div>
<div>
    app.products.pop();
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    const app = new Vue({
        el:'#app',
        data:{
            product:'Boots',
            products:[
                'Boots',
                'Jacket'
            ]
        },
        computed:{
            totalProducts(){
                return this.products.reduce((sum,product) => {
                    return sum + product.quantity;
                },0);
            }
        },
        created(){
            fetch('http://localhost:8080/hello').then(response=> response.json()).then(json => {
                console.log("0");
                this.products = json.products;
            })  
            fetch('http://localhost:8080/hello').then(response=> response.json()).then(json => {
                console.log("1");
            })  
        }
    })
</script>
</body>
</html>

按照我的理解是應該先輸出0,後輸出1。但事實上並非如此,因為fetch是非同步的,這裡的非同步我們可以簡單的理解為執行fetch的執行者並不會馬上執行,所以會出現1先在控制檯輸出這樣的情況:

實際輸出結果

但我的願望是在第一個fetch任務執行之後再執行第二個fetch任務, 其實可以這麼寫:

fetch('http://localhost:8080/hello').then(response=> response.json()).then(json => {
                console.log("0");
                this.products = json.products;
                fetch('http://localhost:8080/hello').then(response=> response.json()).then(json => {
                console.log("1");
      })  
})  

這看起來像是一種解決方案,那如果說我有A、B、C、D四個任務都是非同步的都想按A B C D這樣的順序寫,如果向上面的寫法,巢狀呼叫,那看起來就相當的不體面。由此就引出了promise。

promise

基本使用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>    
  new Promise(function(resolve, reject) {
              setTimeout(()=>{
                    console.log(0);
                    resolve("1");    
               },3000)
     }).then((result)=> console.log(result)); 
</script>
</html> 

如上面程式碼所示,我們宣告瞭一個Promise物件,並向其傳遞了一個函式,函式的行為是3秒後再控制檯輸出0,並呼叫resolve函式,resolve函式會觸發then()。所以我們上面的兩個fetch用promise就可以這麼寫。

fetch('http://localhost:8080/hello').then(response=> response.json()).then(json => {
   console.log("0");
   this.products = json.products;
               
}).then(()=>{
     fetch('http://localhost:8080/hello').then(response=> response.json()).then(json => {
         console.log("1");
     })  
})  

fetch直接返回的就是Promise物件,then之後也還是一個Promise物件。Promise物件的建構函式語法如下:

let promise = new Promise(function(resolve, reject) {
 
});

對於一個任務來說,任務最終狀態有兩種: 一種是執行成功、一種是執行失敗。引數resolve和reject是由JavaScript自身提供的回撥,是兩個函式, 由 JavaScript 引擎預先定義,因此我們只需要在任務的成功的時候呼叫resolve,失敗的時候reject即可。注意任務的最終狀態只能是成功或者失敗,你不能先呼叫resolve後呼叫reject,這樣任務就會被認定為失敗狀態。注意,如果任務在執行的過程中出現了什麼問題,那麼應該呼叫reject,你可以傳遞給reject任意型別的引數,但是建議使用Error物件或繼承自Error的物件。

Promise的狀態

Promise狀態

一個Promise剛建立任務未執行完畢,狀態處於pending。在任務執行完畢呼叫resolve時,狀態轉為fulfilled。任務執行出現異常呼叫error,狀態轉為reject。

then、catch、finally

then的中文意為然後,連貫理解起來也比較自然,任務執行完畢然後執行,語法如下:

promise.then(
  function(result) { /* 處理任務正常執行的結果 */ }, 
  function(error) { /* 處理任務異常執行的結果 */ }
);

第一個函式將在 promise resolved 且接收到結果後執行。第二個函式將在 promise rejected 且接收到error資訊後執行。

下面是示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
    let promiseSuccess = new Promise(function(resolve,reject){
        setTimeout(() => resolve("任務正確執行完畢"),1000);    
    })
    promiseSuccess.then(result=>{
        alert(result);
    });
    let promiseFailed = new Promise(function(resolve,reject){
        setTimeout(() => reject("任務執行出現異常"),1000);    
    })
    promiseFailed.then(result=>{
        alert(result);
    },error=>{
        alert(error);
    });
</script>
</html>

如果你只想關注任務執行發生異常,可以將null作為then的第一個引數, 也可以使用catch:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
    new Promise(function(resolve,reject){
        reject("發生了錯誤");
    }).catch(error=>{
        alert(error);
    })
    new Promise(function(resolve,reject){
        reject("發生了錯誤");
    }).then(null,error=> alert(error));
</script>
</html>

像try catch 中的finally子句一樣,promise也有finally,不同於then,finally沒有引數,我們不知道promise是否成功,不過關係,finally的設計意圖是執行“常規”的完成程式。當promise中的任務結束,也就是呼叫resolve或reject,finally將會被執行:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
    new Promise(function(resolve,rejetc){
          resolve("hello");   
    }).then(result => alert(result),null).finally(()=>alert("執行清理工作中"));
</script>
</html>

then catch finally 的執行順序為跟在Promise後面的順序。

Promise鏈與錯誤處理

透過上面的討論,我們已經對Promise有一個基本的瞭解了,如果我們有一系列非同步任務,為了討論方便讓我們把它稱之為A、B、C、D,非同步任務的特點是雖然他們在程式碼中的順序是A、B、C、D,但實際執行順序可能四個字母的排列組合,但是我們希望是A、B、C、D,那我們用Promise可以這麼寫:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
    new Promise(function(resolve,reject){
        alert("我是A任務");
        resolve("請B任務執行");
    }).then(result=>{
       alert(result+"收到請求,B任務開始執行");
       return "B 任務執行完畢,請C任務開始執行";
    },null).then(result=>{
        alert(result+"收到請求,C任務開始執行");
        return "C 任務執行完畢,請D任務開始執行";
    },null).then(result =>{
          alert(result+"D任務開始執行,按A、B、C、D序列執行完畢");
    },null)
</script>
</html>

執行流程為: 初始化promise,然後開始執行alert後resolve,此時結果滑動到最近的處理程式then,then處理程式被呼叫之後又建立了一個新的Promise,並將值傳給下一個處理程式,以此類推。隨著result在處理程式鏈中傳遞,我們可以看到一系列alert呼叫:

  • 我是A任務
  • 請B任務執行收到請求,B任務開始執行
  • B 任務執行完畢,請C任務開始執行收到請求,C任務開始執行
  • C 任務執行完畢,請D任務開始執行D任務開始執行,按A、B、C、D序列執行完畢

這樣做之所以是可行的,是因為每個對.then的呼叫都會返回了一個新的promise,因此我們可以在其之上呼叫下一個.then。

當處理程式返回一個值時(then),它將成為該promise的result,所以將使用它呼叫下一個.then。

上面的是按A、B、C、D執行,我們也會期待A任務執行之後,B、C、D都開始執行,這類似於跑步比賽,A是開發令槍的人,聽到訊號,B、C、D開始跑向終點。下面是一個示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
let promise  = new Promise(function(resolve,reject){
    setTimeout(() => resolve(1),1000);
})

promise.then(function(result){
    alert(result); // 1
    return result * 2;
})

promise.then(function(result){
    alert(result); // 1
    return result * 2;
})

promise.then(function(result){
    alert(result); // 1
    return result * 2;
})

</script>
</html>

彈出結果將只會是1。

Promise API

Promise.all

假設我們希望並行執行多個任務,並且等待這些任務執行完畢才進行下一步,Promise為我們提供了Promise.all:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
   var  proOne  = new Promise(resolve=> setTimeout(()=> resolve(1),3000);
   var  proTwo  = new Promise(resolve=> setTimeout(()=> resolve(2),2000);
   var proThree = new Promise(resolve=> setTimeout(()=> resolve(3),1000));
   Promise.all([proOne,proTwo,proThree]).then(alert);
</script>
</html>

即使proOne任務花費時間最長,但它的結果仍讓是陣列中的第一個。如果人一個promise失敗即呼叫了reject,那麼Promise.all就被理解reject,完全忽略列表中其他的promise:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
   var  proOne  = new Promise(resolve=> setTimeout(()=> resolve(1),1000));
   var  proTwo  = new Promise((resolve,reject)=> setTimeout(()=> reject(2),1000));
   var  proThree = new Promise(resolve=> setTimeout(()=> resolve(3),1000));
   Promise.all([proOne,proTwo,proThree]).catch(alert);
</script>
</html>

最後會彈出2。

Promise的語法如下:

// iterable 代表可迭代物件
let promise = Promise.all(iterable);

iterable 可以出現非Promise型別的值如下圖所示:

Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  }),
  2,
  3
]).then(alert); // 1, 2, 3

Promise.allSettled

Promise.all中任意一個promise的失敗,會導致最終的任務失敗,但是有的時候我們希望看到所有任務的執行結果,有三個任務A、B、C執行,即使A失敗了,那麼B和C的結果我們也是關注的,由此就引出了Promise.allSettled:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
// 這三個地址用於獲取使用者資訊。
// 即使一個失敗了我們仍然對其他的感興趣
let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://no-such-url'
];
Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { // (*)
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
         alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });
</script>
</html>

注意Promise.allSettled是新新增的特性,舊的瀏覽器可能不支援,那就需要我們包裝一下,更為專業的說法是polyfill:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
    if(!Promise.allSettled){
        const rejectHandler = reason => ({status:'rejected',reason});
        const resolveHandler = value => ({status:'fulfilled',value});

        Promise.allSettled = function(promises){
            const convertedPromises = promises.map(p => Promise.resolve(p)).then(resolveHandler,rejectHandler);
            Promise.all(convertedPromises);
        }
    }
</script>
</html>

promises透過map中的Promise.resolve(p)將輸入值轉換為Promise,然後對每個Promise都新增.then處理程式。這樣我們就可以使用Promise.allSettled來獲取所有給定的promise的結果,即使其中一些被reject。

Promise.race

有的時候我們也只關注第一名,那該怎麼做呢? 由此引出Promise.race:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
 Promise.race([
    new Promise((resolve,reject) => setTimeout(()=> resolve(1),1000)),
    new Promise((resolve,reject) => setTimeout(()=> resolve(2),2000)),   
    new Promise((resolve,reject) => setTimeout(()=> resolve(3),2000))
 ]).then(alert);
</script>
</html>

這裡第一個Promise最快,所以alert(1)。

Promise.any

但最快有可能是犯規的對嗎? 即這個任務reject,有的時候我們只想關注沒有違規的第一名,由此引出Promise.any:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
 Promise.race([
    new Promise((resolve,reject) => setTimeout(()=> reject(1),1000)),
    new Promise((resolve,reject) => setTimeout(()=> resolve(2),2000)),   
    new Promise((resolve,reject) => setTimeout(()=> resolve(3),2000))
 ]).then(alert);
</script>
</html>

雖然第一個promise最快,但是它犯規了,所以alert的是2。那要是全犯規了呢, 那就需要catch一下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=S, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
     Promise.any([
    new Promise((resolve,reject) => setTimeout(()=> reject("犯規1"),1000)),
    new Promise((resolve,reject) => setTimeout(()=> reject("犯規2"),2000))
 ]).catch(error =>{
    console.log(error.constructor.name); // AggregateError
    console.log(error.errors[0]); // 控制檯輸出犯規1
     console.log(error.errors[1]); // 控制檯輸出犯規2
  } )
</script>
</html>

async/await 簡介

有的時候非同步方法可能比較龐大,直接用Promise寫可能不大雅觀,由此我們就引出async/await, 我們只用在需要用到Promise的非同步函式上寫上async這個關鍵字,這個函式的返回值將會自動的被包裝進Promise:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
async function f() {
  return 1;
}
f().then(alert); // 1
</script>
</html>

但是我們常常會碰到這樣一種情況, 我們封裝了一個函式,這個函式上有async,但是內部的實現有兩個操作A、B,B操作需要等到A完成之後再執行,由此就引出了await:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=
    , initial-scale=1.0">
    <title>Document</title>
</head>
<body>
</body>
<script>
async function demo(){
     let responseOne = fetch('http://localhost:8080/hello');
     console.log("1");
     let json  = await responseOne.json; // 語句一
     let response = fetch('http://localhost:8080/hello');
     let jsonTwo  = await responseOne.json; // 語句二
     console.log("2");
}
demo(); 
</script>
</html>

這裡用async/wait改寫了我們開頭的例子,JavaScript引擎執行到語句一的時候會等待fetch操作完成,才執行await後的程式碼。這一看好像清爽了許多。注意await不能在沒有async的函式中執行。相比較於Promise.then,它只是獲取promise結果的一個更加優雅的寫法,並且看起來更易於讀寫。那如果等待的promise發生了異常呢,該怎麼進行錯誤處理呢?有兩種選擇,一種是try catch,另一種是用非同步函式返回的promise產生的catch來處理這個錯誤:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=
    , initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script>
async function demo01(){
    try{
         let response = await fetch('/no-user-here');
         let user = await response.json();
    }catch(error){
        alert(error);
    }
}
demo01(); 

async function demo02(){
    let response = await fetch('/no-user-here');
    let user = await response.json();
    
}
demo02().catch(alert);
</script>
</html>

總結一下

JavaScript也有非同步任務,如果我們想協調非同步任務,就可以選擇使用Promise,當前的我們只有一個非同步任務,我們希望在非同步任務回撥,那麼可以在用Promise.then,如果你想關注錯誤結果,那麼可以用catch,如果你想在任務完成之後做一些清理工作,那麼可以用Promise的finally。現在我們將非同步任務的數目提升,提升到三個,如果我們想再這三個任務完成之後觸發一些操作,那麼我們可以用Promise.all,但是但Promise.all的缺陷在於,一個任務失敗之後,我們看不到成功任務的結果,如果任務成功與失敗的結果,那麼就可以用Promise.allSettled。但有的時候我們也指向關注“第一名”,那就用Promise.race,但有的時候我們也只想要沒犯規的第一名,這也就是Promise.any。有的時候我們也不想用then回撥的這種方式,這寫起來可能有點煩,那就可以用async/await 。

參考資料

相關文章