20分鐘帶你掌握JavaScript Promise和 Async/Await

葡萄城技術團隊發表於2020-12-11

轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。
原文出處:https://www.freecodecamp.org/news/learn-promise-async-await-in-20-minutes/

 

一般在開發中,查詢網路API操作時往往是比較耗時的,這意味著可能需要一段時間的等待才能獲得響應。因此,為了避免程式在請求時無響應的情況,非同步程式設計就成為了開發人員的一項基本技能。

在JavaScript中處理非同步操作時,通常我們經常會聽到 "Promise "這個概念。但要理解它的工作原理及使用方法可能會比較抽象和難以理解。

那麼,在本文中我們將會通過實踐的方式讓你能更快速的理解它們的概念和用法,所以與許多傳統乾巴巴的教程都不同,我們將通過以下四個示例開始:

  • 示例1:用生日解釋Promise的基礎知識
  • 示例2:一個猜數字的遊戲
  • 示例3:從Web API中獲取國家資訊
  • 示例4:從Web API中獲取一個國家的周邊國家列表

 

示例1:用生日解釋Promise基礎知識

首先,我們先來看看Promise的基本形態是什麼樣的。

Promise執行時分三個狀態:pending(執行中)、fulfilled(成功)、rejected(失敗)。

new Promise(function(resolve, reject) {
    if (/* 非同步操作成功 */) {
        resolve(value); //將Promise的狀態由padding改為fulfilled
    } else {
        reject(error); //將Promise的狀態由padding改為rejected
    }
})
實現時有三個原型方法then、catch、finally
promise
.then((result) => {
	//promise被接收或拒絕繼續執行的情況
})
.catch((error) => {
	//promise被拒絕的情況
})
.finally (() => {
	//promise完成時,無論如何都會執行的情況
})

 

基本形態介紹完成了,那麼我們下面開始看看下面的示例吧。

使用者故事:我的朋友Kayo答應在兩週後在我的生日Party上為我做一個蛋糕。

如果一切順利且Kayo沒有生病的話,我們就會獲得一定數量的蛋糕,但如果Kayo生病了,我們就沒有蛋糕了。但不論有沒有蛋糕,我們仍然會開一個生日Party。

所以對於這個示例,我們將如上的背景故事翻譯成JS程式碼,首先讓我們先建立一個返回Promise的函式。

const onMyBirthday = (isKayoSick) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (!isKayoSick) {
        resolve(2);
      } else {
        reject(new Error("I am sad"));
      }
    }, 2000);
  });
};

 

在JavaScript中,我們可以使用new Promise()建立一個新的Promise,它接受一個引數為:(resolve,reject)=>{} 的函式。

在此函式中,resolve和reject是預設提供的回撥函式。讓我們仔細看看上面的程式碼。

當我們執行onMyBirthday函式2000ms後。

  • 如果Kayo沒有生病,那麼我們就以2為引數執行resolve函式
  • 如果Kayo生病了,那麼我們用new Error("I am sad")作為引數執行reject。儘管您可以將任何要拒絕的內容作為引數傳遞,但建議將其傳遞給Error物件。

現在,因為onMyBirthday()返回的是一個Promise,我們可以訪問then、catch和finally方法。我們還可以訪問早些時候在then和catch中使用傳遞給resolve和reject的引數。

讓我們通過如下程式碼來理解概念

如果Kayo沒有生病

onMyBirthday(false)
  .then((result) => {
    console.log(`I have ${result} cakes`); // 控制檯列印“I have 2 cakes”  
  })
  .catch((error) => {
    console.log(error); // 不執行
  })
  .finally(() => {
    console.log("Party"); // 控制檯列印“Party”
  });

 

如果Kayo生病

onMyBirthday(true)
  .then((result) => {
    console.log(`I have ${result} cakes`); // 不執行 
  })
  .catch((error) => {
    console.log(error); // 控制檯列印“我很難過”
  })
  .finally(() => {
    console.log("Party"); // 控制檯列印“Party”
  });

 

相信通過這個例子你能瞭解Promise的基本概念。

下面我們開始示例2

示例2:一個猜數字的遊戲

基本需求:

  • 使用者可以輸入任意數字
  • 系統從1到6中隨機生成一個數字
  • 如果使用者輸入數字等於系統隨機數,則給使用者2分
  • 如果使用者輸入數字與系統隨機數相差1,給使用者1分,否則,給使用者0分
  • 使用者想玩多久就玩多久

對於上面的需求,我們首先建立一個enterNumber函式並返回一個Promise:

const enterNumber = () => {
  return new Promise((resolve, reject) => {
    // 從這開始編碼
  });
};

我們要做的第一件事是向使用者索要一個數字,並在1到6之間隨機選擇一個數字:

const enterNumber = () => {
  return new Promise((resolve, reject) => {
    const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // 向使用者索要一個數字
    const randomNumber = Math.floor(Math.random() * 6 + 1); // 選擇一個從1到6的隨機數
  });
};

當使用者輸入一個不是數字的值。這種情況下,我們呼叫reject函式,並丟擲錯誤:

const enterNumber = () => {
  return new Promise((resolve, reject) => {
    const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // 向使用者索要一個數字
    const randomNumber = Math.floor(Math.random() * 6 + 1); //選擇一個從1到6的隨機數

    if (isNaN(userNumber)) {
      reject(new Error("Wrong Input Type")); // 當使用者輸入的值非數字,丟擲異常並呼叫reject函式
    }
  });
};

下面,我們需要檢查userNumber是否等於RanomNumber,如果相等,我們給使用者2分,然後我們可以執行resolve函式來傳遞一個object { points: 2, randomNumber } 物件。

如果userNumber與randomNumber相差1,那麼我們給使用者1分。否則,我們給使用者0分。

return new Promise((resolve, reject) => {
  const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // 向使用者索要一個數字
  const randomNumber = Math.floor(Math.random() * 6 + 1); // 選擇一個從1到6的隨機數

  if (isNaN(userNumber)) {
    reject(new Error("Wrong Input Type")); // 當使用者輸入的值非數字,丟擲異常並呼叫reject函式
  }

  if (userNumber === randomNumber) {
    // 如果相等,我們給使用者2分
    resolve({
      points: 2,
      randomNumber,
    });
  } else if (
    userNumber === randomNumber - 1 ||
    userNumber === randomNumber + 1
  ) {
    // 如果userNumber與randomNumber相差1,那麼我們給使用者1分
    resolve({
      points: 1,
      randomNumber,
    });
  } else {
    // 否則使用者得0分
    resolve({
      points: 0,
      randomNumber,
    });
  }
});

下面,讓我們再建立一個函式來詢問使用者是否想繼續遊戲:

const continueGame = () => {
  return new Promise((resolve) => {
    if (window.confirm("Do you want to continue?")) { // 向使用者詢問是否要繼續遊戲
      resolve(true);
    } else {
      resolve(false);
    }
  });
};

為了不使遊戲強制結束,我們建立的Promise沒有使用Reject回撥。

下面,我們建立一個函式來處理猜數字邏輯:

const handleGuess = () => {
  enterNumber() // 返回一個Promise物件
    .then((result) => {
      alert(`Dice: ${result.randomNumber}: you got ${result.points} points`); // 當resolve執行時,我們得到使用者得分和隨機數 
      
      // 向使用者詢問是否要繼續遊戲
      continueGame().then((result) => {
        if (result) {
          handleGuess(); // If yes, 遊戲繼續
        } else {
          alert("Game ends"); // If no, 彈出遊戲結束框
        }
      });
    })
    .catch((error) => alert(error));
};

handleGuess(); // 執行handleGuess 函式

 

在這當我們呼叫handleGuess函式時,enterNumber()返回一個Promise物件。

如果Promise狀態為resolved,我們就呼叫then方法,向使用者告知競猜結果與得分,並向使用者詢問是否要繼續遊戲。

如果Promise狀態為rejected,我們將顯示一條使用者輸入錯誤的資訊。

不過,這樣的程式碼雖然能解決問題,但讀起來還是有點困難。讓我們後面將使用async/await 對hanldeGuess進行重構。

網上對於 async/await 的解釋已經很多了,在這我想用一個簡單概括的說法來解釋:async/await就是可以把複雜難懂的非同步程式碼變成類同步語法的語法糖

下面開始看重構後程式碼吧:

const handleGuess = async () => {
  try {
    const result = await enterNumber(); // 代替then方法,我們只需將await放在promise前,就可以直接獲得結果

    alert(`Dice: ${result.randomNumber}: you got ${result.points} points`);

    const isContinuing = await continueGame();

    if (isContinuing) {
      handleGuess();
    } else {
      alert("Game ends");
    }
  } catch (error) { // catch 方法可以由try, catch函式來替代
    alert(error);
  }
};

通過在函式前使用async關鍵字,我們建立了一個非同步函式,在函式內的使用方法較之前有如下不同:

  • 和then函式不同,我們只需將await關鍵字放在Promise前,就可以直接獲得結果。
  • 我們可以使用try, catch語法來代替promise中的catch方法。

下面是我們重構後的完整程式碼,供參考:  

const enterNumber = () => {
  return new Promise((resolve, reject) => {
    const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // 向使用者索要一個數字
    const randomNumber = Math.floor(Math.random() * 6 + 1); // 系統隨機選取一個1-6的數字

    if (isNaN(userNumber)) {
      reject(new Error("Wrong Input Type")); // 如果使用者輸入非數字丟擲錯誤
    }

    if (userNumber === randomNumber) { // 如果使用者猜數字正確,給使用者2分
      resolve({
        points: 2,
        randomNumber,
      });
    } else if (
      userNumber === randomNumber - 1 ||
      userNumber === randomNumber + 1
    ) { // 如果userNumber與randomNumber相差1,那麼我們給使用者1分
      resolve({
        points: 1,
        randomNumber,
      });
    } else { // 不正確,得0分
      resolve({
        points: 0,
        randomNumber,
      });
    }
  });
};

const continueGame = () => {
  return new Promise((resolve) => {
    if (window.confirm("Do you want to continue?")) { // 向使用者詢問是否要繼續遊戲
      resolve(true);
    } else {
      resolve(false);
    }
  });
};

const handleGuess = async () => {
  try {
    const result = await enterNumber(); // await替代了then函式

    alert(`Dice: ${result.randomNumber}: you got ${result.points} points`);

    const isContinuing = await continueGame();

    if (isContinuing) {
      handleGuess();
    } else {
      alert("Game ends");
    }
  } catch (error) { // catch 方法可以由try, catch函式來替代
    alert(error);
  }
};

handleGuess(); // 執行handleGuess 函式

我們已經完成了第二個示例,接下來讓我們開始看看第三個示例。

示例3:從Web API中獲取國家資訊

一般當從API中獲取資料時,開發人員會精彩使用Promises。如果在新視窗開啟https://restcountries.eu/rest/v2/alpha/cn,你會看到JSON格式的國家資料。

通過使用Fetch API,我們可以很輕鬆的獲得資料,以下是程式碼:

const fetchData = async () => {
  const res = await fetch("https://restcountries.eu/rest/v2/alpha/cn"); // fetch() returns a promise, so we need to wait for it

  const country = await res.json(); // res is now only an HTTP response, so we need to call res.json()

  console.log(country); // China's data will be logged to the dev console
};

fetchData();

現在我們獲得了所需的國家/地區資料,讓我們轉到最後一項任務。

示例4:從Web API中獲取一個國家的周邊國家列表

下面的fetchCountry函式從示例3中的api獲得國家資訊,其中的引數alpha3Code 是代指該國家的國家程式碼,以下是程式碼

// Task 4: 獲得中國周邊的鄰國資訊
const fetchCountry = async (alpha3Code) => {
  try {
    const res = await fetch(
      `https://restcountries.eu/rest/v2/alpha/${alpha3Code}`
    );

    const data = await res.json();

    return data;
  } catch (error) {
    console.log(error);
  }
};

下面讓我們建立一個fetchCountryAndNeighbors函式,通過傳遞cn作為alpha3code來獲取中國的資訊。

const fetchCountryAndNeighbors = async () => {
  const china= await fetchCountry("cn");

  console.log(china);
};

fetchCountryAndNeighbors();

在控制檯中,我們看看物件內容:  

 

 

在物件中,有一個border屬性,它是中國周邊鄰國的alpha3codes列表。

現在,如果我們嘗試通過以下方式獲取鄰國資訊。

const neighbors = 
    china.borders.map((border) => fetchCountry(border));

 

neighbors是一個Promise物件的陣列。

當處理一個陣列的Promise時,我們需要使用Promise.all。

const fetchCountryAndNeigbors = async () => {
  const china = await fetchCountry("cn");

  const neighbors = await Promise.all(
    china.borders.map((border) => fetchCountry(border))
  );

  console.log(neighbors);
};

fetchCountryAndNeigbors();

 

在控制檯中,我們應該能夠看到國家/地區物件列表。

 

以下是示例4的所有程式碼,供您參考:

const fetchCountry = async (alpha3Code) => {
  try {
    const res = await fetch(
      `https://restcountries.eu/rest/v2/alpha/${alpha3Code}`
    );
    const data = await res.json();
    return data;
  } catch (error) {
    console.log(error);
  }
};

const fetchCountryAndNeigbors = async () => {
  const china = await fetchCountry("cn");
  const neighbors = await Promise.all(
    china.borders.map((border) => fetchCountry(border))
  );
  console.log(neighbors);
};

fetchCountryAndNeigbors();

  

總結

完成這4個示例後,你可以看到Promise在處理非同步操作或不是同時發生的事情時很有用。相信在不斷的實踐中,對它的理解會越深、越強,希望這篇文章能對大家理解Promise和Async/Await帶來一些幫助。

以下是本文中使用的程式碼:

 

 

https://files.cnblogs.com/files/powertoolsteam/Promise-Async-Await-main.zip

 

相關文章