異步編程是一種編程範式,允許您編寫可以以異步方式運行的代碼。與同步編程相比,同步執行代碼,異步編程允許代碼在背景中運行,同時程序的其餘部分繼續執行。這對於可能需要很長時間才能完成的任務特別有用,例如從遠程API獲取數據。

異步編程對於在JavaScript中創建響應迅速且高效的應用程序至關重要。 TypeScript是JavaScript的一個超集,使得使用異步編程變得更加容易。

在TypeScript中有幾種處理異步編程的方法,包括使用promisesasync/awaitcallbacks。我們將詳細介紹這些方法,讓您可以為您的用例選擇最適合的方法。

目錄

  1. 為什麼異步編程很重要?

  2. TypeScript如何使異步編程變得更簡單

  3. 如何在 TypeScript 中使用 Promises

  4. 如何在 TypeScript 中使用 Async / Await

  5. 如何在 TypeScript 中使用回調函數

  6. 結論

為什麼異步編程很重要?

異步編程對於構建響應迅速且高效的網絡應用程式至關重要。它允許任務在後台運行,同時程序的其餘部分繼續運行,保持用戶界面對輸入的響應。此外,異步編程可以通過同時運行多個任務來提高整體性能。

有許多現實世界的異步編程示例,例如訪問用戶攝像頭和麥克風以及處理用戶輸入事件。即使您不經常創建異步函數,瞭解如何正確使用它們以確保應用程序可靠且性能良好是很重要的。

TypeScript如何使異步編程更加簡單

TypeScript提供了幾個功能,可以簡化異步編程,包括類型安全類型推斷類型檢查類型注釋

通過類型安全,您可以確保代碼的行為符合預期,即使處理異步函數時也是如此。例如,TypeScript可以在編譯時檢測與空值和未定義值相關的錯誤,節省了您在調試方面的時間和精力。

TypeScript的類型推斷和檢查還可以減少您需要編寫的樣板代碼量,使您的代碼更加簡潔且易於閱讀。

此外,TypeScript的類型註釋為您的代碼提供了清晰的說明和文檔,特別是在處理可能難以理解的異步函數時尤為有用。

現在讓我們深入了解異步編程的三個關鍵特性:承諾、async/await 和回調函數。

如何在 TypeScript 中使用承諾

承諾是處理 TypeScript 中異步操作的強大工具。例如,您可能會使用承諾從外部 API 獲取數據,或在主線程持續運行的同時在背景中執行耗時的任務。

要使用承諾,您需要創建一個新的 Promise 類的實例,並傳遞一個執行異步操作的函數。當操作成功時,這個函數應該調用 resolve 方法並傳入結果;如果失敗,則調用 reject 方法並傳入錯誤。

一旦創建了承諾,您可以使用 then 方法將回調函數附加到它上面。這些回調函數將在承諾被滿足時觸發,解析的值將作為參數傳遞。如果承諾被拒絕,您可以使用 catch 方法附加一個錯誤處理函數,該函數將以拒絕的原因為參數被調用。

使用承諾相對於傳統的基於回調的方法提供了幾個優勢。例如,承諾可以幫助防止“回調地獄”,這是異步代碼中的一個常見問題,其中嵌套的回調變得難以閱讀和維護。

承諾還使異步代碼中的錯誤處理變得更加容易,因為您可以使用 catch 方法來管理發生在承諾鏈中的任何地方的錯誤。

最後,承諾可以通過提供一致且可組合的方式來處理異步操作來簡化您的代碼,無論其底層實現如何。

如何創建一個 Promise

Promise 語法:

const myPromise = new Promise((resolve, reject) => {
  // 執行一些非同步操作
  // 如果操作成功,使用結果調用 resolve
  // 如果操作失敗,使用錯誤對象調用 reject
});

myPromise
  .then((result) => {
    // 處理成功的結果
  })
  .catch((error) => {
    // 處理錯誤
  });
// 創建 Promise 的示例 1

function myAsyncFunction(): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    // 一些非同步操作
    setTimeout(() => {
      // 成功的操作解析 Promise 關於在 TypeScript 中精通異步編程的最新博文!學習如何使用 Promises、Async/Await 和 Callbacks 撰寫高效且可擴展的代碼。準備好將您的 TypeScript 技能提升到更高水平吧!
      const success = true;

      if (success) {
        // 如果操作成功,使用操作結果解析 Promise
        resolve(
          `The result is success and your operation result is ${operationResult}`
        );
      } else {
        const rejectCode: number = 404;
        const rejectMessage: string = `The result is failed and your operation result is ${rejectCode}`;
        // 如果操作失敗,使用操作結果拒絕 Promise
        reject(new Error(rejectMessage));
      }
    }, 2000);
  });
}

// 使用 Promise
myAsyncFunction()
  .then((result) => {
    console.log(result); // 輸出:結果為成功,您的操作結果為 4
  })
  .catch((error) => {
    console.error(error); // 輸出:結果為失敗,您的操作結果為 404
  });

在上述範例中,我們有一個名為 myAsyncFunction() 的函數,該函數返回一個 promise。我們使用 Promise 構造函數來創建 promise,該構造函數接受一個包含 resolvereject 參數的 callback function。如果異步操作成功,我們調用 resolve 函數。如果失敗,我們調用 reject 函數。

構造函數返回的 promise 物件有一個 then() 方法,該方法接受成功和失敗的回調函數。如果 promise 成功解析,則成功的回調函數將與結果一起被調用。如果 promise 被拒絕,則失敗的回調函數將與錯誤信息一起被調用。

promise 物件還有一個 catch() 方法,用於處理在 promise 鏈中發生的錯誤。catch() 方法接受一個回調函數,如果在 promise 鏈中發生任何錯誤,則會調用該回調函數。

現在,讓我們來看看如何在 TypeScript 中鏈接 promises。

如何鏈接 Promises

鏈接 promises 允許你按順序或平行執行 多個異步操作。這在你需要一個接一個或同時執行幾個異步任務時非常有幫助。例如,你可能需要異步獲取數據,然後再異步處理它。

讓我們看看如何鏈接 promises 的範例:

// 鏈式承諾的工作範例
// 第一個承諾
const promise1 = new Promise((resolve, reject) => {
  const functionOne: string = "This is the first promise function";
  setTimeout(() => {
    resolve(functionOne);
  }, 1000);
});

// 第二個承諾
const promise2 = (data: number) => {
  const functionTwo: string = "This is the second second promise  function";
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(` ${data}  '+'  ${functionTwo} `);
    }, 1000);
  });
};

// 將第一個和第二個承諾鏈接在一起
promise1
  .then(promise2)
  .then((result) => {
    console.log(result); // 輸出:這是第一個承諾函數 + 這是第二個承諾函數
  })
  .catch((error) => {
    console.error(error);
  });

在上述範例中,我們有兩個承諾: promise1promise2promise1 在1秒後解析,返回字符串 “這是第一個承諾函數。” promise2 接受一個數字作為輸入,並返回一個在1秒後解析的承諾,該承諾返回一個結合輸入數字和字符串 “這是第二個承諾函數。” 的字符串。

我們使用 then 方法將這兩個承諾鏈接在一起。promise1 的輸出作為輸入傳遞給 promise2。最後,我們再次使用 then 方法將 promise2 的輸出記錄到控制台。如果 promise1promise2 之一被拒絕,錯誤將由 catch 方法捕獲。

恭喜!你已經學會了如何在 TypeScript 中創建和鏈接承諾。現在你可以使用承諾來執行 TypeScript 中的非同步操作。現在,讓我們探索 Async/Await 在 TypeScript 中的工作原理。

如何在 TypeScript 中使用 Async / Await

Async/await 是在 ES2017 中引入的一種語法,用於使處理 Promises 更加簡便。它允許你編寫看起來和感覺像同步代碼的異步代碼。

在 TypeScript 中,你可以使用 async 關鍵字定義一個異步函數。這告訴編譯器該函數是異步的,並將返回一個 Promise。

現在,讓我們看看如何在 TypeScript 中使用 async/await。

Async / Await 語法:

// TypeScript 中的 Async / Await 語法
async function functionName(): Promise<ReturnType> {
  try {
    const result = await promise;
    // 在 Promise 解決後執行的代碼
    return result;
  } catch (error) {
    // 在 Promise 拒絕時執行的代碼
    throw error;
  }
}

在上面的例子中,functionName 是一個異步函數,返回 ReturnType 的 Promise。await 關鍵字用於等待 Promise 解決後再執行下一行代碼。

try/catch 區塊用於處理在執行異步函數內部代碼時發生的任何錯誤。如果發生錯誤,它將被 catch 區塊捕獲,你可以在那裡適當地處理它。

使用箭頭函數與 Async / Await

你也可以在 TypeScript 中使用箭頭函數與 async/await 語法:

const functionName = async (): Promise<ReturnType> => {
  try {
    const result = await promise;
    // 在 Promise 解決後執行的代碼
    return result;
  } catch (error) {
    // 在 Promise 拒絕時執行的代碼
    throw error;
  }
};

在上面的範例中,functionName 被定義為一個箭頭函數,返回一個 ReturnType 的 Promise。async 關鍵字表示這是一個異步函數,而 await 關鍵字用來等待 Promise 解決後再進入下一行代碼。

使用 Async / Await 進行 API 調用

現在,讓我們超越語法,使用 async/await 從 API 獲取一些數據。

interface User {
  id: number;
  name: string;
  email: string;
}

const fetchApi = async (): Promise<void> => {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");

    if (!response.ok) {
      throw new Error(
        `Failed to fetch users (HTTP status code: ${response.status})`
      );
    }

    const data: User[] = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
    throw error;
  }
};

fetchApi();

在這裡,我們從 JSONPlaceholder API 獲取數據,將其轉換為 JSON,然後將其記錄到控制台。這是一個在 TypeScript 中如何使用 async/await 的實際例子。

您應該會在控制台中看到用戶信息。這張圖片顯示了輸出:

使用 Axios API 調用的 Async/Await

// 示例 2 如何在 TypeScript 中使用 async / await

const fetchApi = async (): Promise<void> => {
  try {
    const response = await axios.get(
      "https://jsonplaceholder.typicode.com/users"
    );
    const data = await response.data;
    console.log(data);
  } catch (error) {
    console.error(error);
  }
};

fetchApi();

在上面的範例中,我們使用 async/await 定義了 fetchApi() 函數,並使用 Axios.get() 方法對指定的 URL 發送 HTTP GET 請求。我們使用 await 等待響應,然後通過響應對象的數據屬性提取數據。最後,我們使用 console.log() 將數據記錄到控制台中。任何發生的錯誤都會被捕獲並使用 console.error() 記錄到控制台。

我們可以使用 Axios 來實現這一點,因此您應該在控制台中看到相同的結果。

這張圖片顯示了使用 Axios 時在控制台中的輸出:

注意:在您嘗試上面的代碼之前,您需要使用 npm 或 yarn 安裝 Axios。


npm install axios

yarn add axios

如果您不熟悉 Axios,您可以 在這裡了解更多

您可以看到我們使用了 trycatch 區塊來處理錯誤。trycatch 區塊是在 TypeScript 中管理錯誤的一種方法。因此,無論何時您進行像我們剛才那樣的 API 呼叫,都確保使用 trycatch 區塊來處理任何錯誤。

現在,讓我們探索在 TypeScript 中使用 trycatch 區塊的更高級用法:

// 示例 3:如何在 TypeScript 中使用 async / await

interface Recipe {
  id: number;
  name: string;
  ingredients: string[];
  instructions: string[];
  prepTimeMinutes: number;
  cookTimeMinutes: number;
  servings: number;
  difficulty: string;
  cuisine: string;
  caloriesPerServing: number;
  tags: string[];
  userId: number;
  image: string;
  rating: number;
  reviewCount: number;
  mealType: string[];
}

const fetchRecipes = async (): Promise<Recipe[] | string> => {
  const api = "https://dummyjson.com/recipes";
  try {
    const response = await fetch(api);

    if (!response.ok) {
      throw new Error(`Failed to fetch recipes: ${response.statusText}`);
    }

    const { recipes } = await response.json();
    return recipes; // 返回食譜數組
  } catch (error) {
    console.error("Error fetching recipes:", error);
    if (error instanceof Error) {
      return error.message;
    }
    return "An unknown error occurred.";
  }
};

// 獲取並記錄食譜
fetchRecipes().then((data) => {
  if (Array.isArray(data)) {
    console.log("Recipes fetched successfully:", data);
  } else {
    console.error("Error message:", data);
  }
});

在上面的示例中,我們定義了一個 interface Recipe,該接口概述了我們期望從 API 獲得的數據結構。然後,我們使用 async/await 和 fetch() 方法創建 fetchRecipes() 函數,以對指定的 API 端點發送 HTTP GET 請求。

我們使用 try/catch 區塊來處理在 API 請求期間可能發生的任何錯誤。如果請求成功,我們使用 await 從響應中提取數據屬性並返回。如果發生錯誤,我們檢查是否有錯誤消息,如果存在則將其作為字符串返回。

最後,我們呼叫 fetchRecipes() 函數並使用 .then() 將返回的數據記錄到控制台。這個範例展示了如何使用 async/await 搭配 try/catch 區塊在更高級的情境中處理錯誤,在這種情境中,我們需要從響應對象中提取數據並返回自定義錯誤消息。

這張圖片顯示了代碼的輸出結果:

使用 Promise.all 的 Async / Await

Promise.all() 是一個方法,接受一個承諾數組作為輸入(可迭代的)並返回一個單一的承諾作為輸出。當所有輸入的承諾都被解決或如果輸入的可迭代中不包含任何承諾時,這個承諾將被解析。如果任何輸入的承諾被拒絕,或如果非承諾的情況下拋出錯誤,它將立即拒絕,並且會以第一個拒絕消息或錯誤進行拒絕。

// 使用 async / await 與 Promise.all 的示例
interface User {
  id: number;
  name: string;
  email: string;
  profilePicture: string;
}

interface Post {
  id: number;
  title: string;
  body: string;
}

interface Comment {
  id: number;
  postId: number;
  name: string;
  email: string;
  body: string;
}

const fetchApi = async <T>(url: string): Promise<T> => {
  try {
    const response = await fetch(url);
    if (response.ok) {
      const data = await response.json();
      return data;
    } else {
      throw new Error(`Network response was not ok for ${url}`);
    }
  } catch (error) {
    console.error(error);
    throw new Error(`Error fetching data from ${url}`);
  }
};

const fetchAllApis = async (): Promise<[User[], Post[], Comment[]]> => {
  try {
    const [users, posts, comments] = await Promise.all([
      fetchApi<User[]>("https://jsonplaceholder.typicode.com/users"),
      fetchApi<Post[]>("https://jsonplaceholder.typicode.com/posts"),
      fetchApi<Comment[]>("https://jsonplaceholder.typicode.com/comments"),
    ]);
    return [users, posts, comments];
  } catch (error) {
    console.error(error);
    throw new Error("Error fetching data from one or more APIs");
  }
};

fetchAllApis()
  .then(([users, posts, comments]) => {
    console.log("Users: ", users);
    console.log("Posts: ", posts);
    console.log("Comments: ", comments);
  })
  .catch((error) => console.error(error));

在上面的代碼中,我們使用 Promise.all 同時抓取多個 API。如果您有多個 API 需要抓取,您可以使用 Promise.all 一次性獲取它們。如您所見,我們使用 map 迴圈遍歷 API 陣列,然後將其傳遞給 Promise.all 以同時抓取它們。

下面的圖片顯示了 API 調用的輸出:

讓我們看看如何與 Axios 一起使用 Promise.all

// 使用 async / await 與 axios 和 Promise.all 的示例

const fetchApi = async () => {
  try {
    const urls = [
      "https://jsonplaceholder.typicode.com/users",
      "https://jsonplaceholder.typicode.com/posts",
    ];
    const responses = await Promise.all(urls.map((url) => axios.get(url)));
    const data = await Promise.all(responses.map((response) => response.data));
    console.log(data);
  } catch (error) {
    console.error(error);
  }
};

fetchApi();

在上面的示例中,我們使用Promise.all同時從兩個不同的URL中獲取數據。首先,我們創建一個URL數組,然後使用map從axios.get調用中創建一個Promise數組。我們將此數組傳遞給Promise.all,它將返回一個響應數組。最後,我們再次使用map從每個響應中獲取數據並將其記錄到控制台中。

如何在TypeScript中使用回調函數

回調函數是作為另一個函數參數傳遞的函數。回調函數在另一個函數內部執行。回調確保一個函數在任務完成之前不運行 – 但在任務完成後立即運行。它們幫助我們編寫異步JavaScript代碼並防止問題和錯誤。

// 在typescript中使用回調函數的示例

const add = (a: number, b: number, callback: (result: number) => void) => {
  const result = a + b;
  callback(result);
};

add(10, 20, (result) => {
  console.log(result);
});

下面的圖像顯示了回調函數:

讓我們看一個在TypeScript中使用回調函數的另一個示例:

// 在TypeScript中使用回調函數的示例

type User = {
  name: string;
  email: string;
};

const fetchUserData = (
  id: number,
  callback: (error: Error | null, user: User | null) => void
) => {
  const api = `https://jsonplaceholder.typicode.com/users/${id}`;
  fetch(api)
    .then((response) => {
      if (response.ok) {
        return response.json();
      } else {
        throw new Error("Network response was not ok.");
      }
    })
    .then((data) => {
      const user: User = {
        name: data.name,
        email: data.email,
      };
      callback(null, user);
    })
    .catch((error) => {
      callback(error, null);
    });
};

// 使用帶有回調函數的fetchUserData
fetchUserData(1, (error, user) => {
  if (error) {
    console.error(error);
  } else {
    console.log(user);
  }
});

在上面的示例中,我們有一個名為fetchUserData的函數,它接受idcallback作為參數。這個callback是一個具有兩個參數的函數:一個錯誤和一個用戶。

fetchUser Data 函數通過 id 從 JSONPlaceholder API 端點檢索用戶數據。如果獲取成功,它將創建一個 User 對象並將其傳遞給回調函數,錯誤為 null。如果在獲取過程中出現錯誤,則將錯誤發送給回調函數,並將用戶設置為 null。

要使用 fetchUser Data 函數與回調,我們提供 id 和回調函數作為參數。回調函數檢查錯誤,如果沒有錯誤,則記錄用戶數據。

下圖顯示了 API 調用的輸出:

如何負責任地使用回調

雖然回調是 TypeScript 異步編程的基礎,但它們需要仔細管理以避免 “回調地獄”——這種金字塔形狀、深度嵌套的代碼會變得難以閱讀和維護。以下是有效使用回調的方法:

  1. 避免深度嵌套

    • 通過將複雜操作拆分為命名函數來扁平化你的代碼結構

    • 對於複雜的異步工作流程,使用 Promise 或 async/await(更多內容見下文)

  2. 錯誤處理優先

    • 始終遵循 Node.js 的 (error, result) 參數約定

    • 在每一層嵌套回調中檢查錯誤

    function processData(input: string, callback: (err: Error | null, result?: string) => void) {
      // ... 總是先用錯誤呼叫回調
    }
  1. 使用類型註解

    • 利用 TypeScript 的類型系統來強制執行回調簽名

    • 為回調參數定義清晰的介面

    type ApiCallback = (error: Error | null, data?: ApiResponse) => void;
  1. 考慮控制流程庫
    對於複雜的異步操作,使用像 async.js 的工具來進行:

    • 並行執行

    • 串行執行

    • 錯誤處理管道

何時使用回調函數與其他選擇

在某些情況下,回調函數是一個很好的選擇,而在其他情況下則不是。

當您在處理非同步操作(單次完成)、與期望回調的舊庫或API接口、處理事件監聽器(如點擊監聽器或websocket事件)或創建具有簡單非同步需求的輕量級工具時,回調函數是有幫助的。

在其他需要專注於編寫可維護代碼且具有清晰非同步流程的情況下,回調函數會帶來麻煩,您應該更傾向於使用承諾或async-await。例如,當您需要鏈接多個操作、處理複雜的錯誤傳播、與現代API(如Fetch API或FS Promises)協作,或使用 promise.all() 進行平行執行時。

從回調函數遷移到承諾的示例:

// 回呼版本
function fetchUser(id: number, callback: (err: Error | null, user?: User) => void) {
  // ... 
}

// 承諾版本
async function fetchUserAsync(id: number): Promise<User> {
  // ...
}

// 使用 async/await
try {
  const user = await fetchUserAsync(1);
} catch (error) {
  // 處理錯誤
}

非同步模式的演變

模式 優點 缺點
回呼 簡單、通用 巢狀複雜度
承諾 可鏈式調用,更好的錯誤流 需要 .then() 鏈
Async/Await 類同步可讀性 需要轉譯

現代 TypeScript 專案經常使用混合方式:對於事件驅動模式使用回呼,對於複雜的非同步邏輯使用承諾/async-await。關鍵是為你的特定用例選擇合適的工具,同時保持代碼的清晰性。

結論

在這篇文章中,我們學習了在 TypeScript 中處理非同步代碼的不同方式。我們學習了回呼、承諾、async/await,以及如何在 TypeScript 中使用它們。我們也學習了這個概念。

如果你想了解更多有關程式設計的知識,以及如何成為更好的軟體工程師,你可以訂閱我的 YouTube 頻道 CliffTech

謝謝您閱讀我的文章。希望您喜歡。如果您有任何問題,請隨時聯繫我。

在社交媒體上與我聯繫: