一般在開發中,查詢網路 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的引數。
讓我們透過如下程式碼來理解概念,相信透過這個例子你能瞭解Promise的基本概念。
如果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"});
示例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 itconst 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帶來一些幫助。
這是本文中使用的程式碼:Asdkjbasrksbr。