异步编程是一种编程范式,允许你编写可以异步运行的代码。与顺序执行代码的同步编程相比,异步编程允许代码在后台运行,同时程序的其余部分继续执行。这对于可能需要很长时间才能完成的任务特别有用,比如从远程API获取数据。

异步编程在JavaScript中创建响应迅速且高效的应用程序至关重要。TypeScript作为JavaScript的超集,使得使用异步编程变得更加容易。

在TypeScript中有几种异步编程的方法,包括使用promisesasync/awaitcallbacks。我们将详细介绍每种方法,以便您可以为您的用例选择最佳方法。

目录

  1. 为什么异步编程很重要?

  2. TypeScript如何使异步编程更加简单

  3. 如何在TypeScript中使用Promises

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

  5. 如何在TypeScript中使用回调函数

  6. 结论

异步编程为何重要?

异步编程对于构建响应迅速、高效的Web应用至关重要。它允许任务在后台运行,同时程序的其余部分继续运行,使用户界面对输入保持响应。此外,异步编程可以通过同时运行多个任务来提高整体性能。

有许多现实世界的异步编程示例,如访问用户摄像头和麦克风以及处理用户输入事件。即使您不经常创建异步函数,了解如何正确使用它们以确保您的应用程序可靠且性能良好也是很重要的。

TypeScript如何使异步编程更容易

TypeScript提供了几个功能,简化了异步编程,包括类型安全类型推断类型检查类型注解

通过类型安全,您可以确保您的代码在处理异步函数时表现如预期。例如,TypeScript可以在编译时捕获与空值和未定义值相关的错误,节省您在调试中的时间和精力。

TypeScript的类型推断和检查还可以减少您需要编写的样板代码量,使您的代码更加简洁易读。

TypeScript的类型注解为您的代码提供清晰的文档,特别是在处理可能难以理解的异步函数时尤为有用。

现在让我们深入了解异步编程的三个关键特性:promises(承诺)、async/await(异步/等待)和callbacks(回调)。

在TypeScript中如何使用Promises

Promise(承诺)是在TypeScript中处理异步操作的强大工具。例如,您可以使用Promise从外部API获取数据,或在后台执行耗时任务,同时主线程继续运行。

要使用Promise,您需要创建一个新的Promise类实例,并传递一个执行异步操作的函数。当操作成功时,该函数应调用resolve方法并传递结果,如果操作失败,则应调用reject方法并传递错误。

创建Promise后,您可以使用then方法附加回调函数。这些回调将在Promise被满足时触发,解析后的值将作为参数传递。如果Promise被拒绝,您可以使用catch方法附加错误处理程序,该处理程序将接收拒绝的原因。

使用Promises相比传统的基于回调的方法有几个优势。例如,Promises可以帮助防止“回调地狱”,这是异步代码中常见的问题,其中嵌套的回调变得难以阅读和维护。

Promise还使得在异步代码中进行错误处理更容易,因为您可以使用catch方法来处理Promise链中任何位置发生的错误。

最后,Promises可以通过提供一种一致且可组合的方式来处理异步操作,简化您的代码,而不管其底层实现如何。

如何创建一个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和回调函数编写高效且可扩展的代码。准备好将你的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参数的回调函数。如果异步操作成功,我们调用resolve函数。如果失败,我们调用reject函数。

构造函数返回的promise对象具有一个then()方法,该方法接受成功和失败的回调函数。如果promise成功解析,成功的回调函数将使用结果调用。如果promise被拒绝,失败的回调函数将使用错误消息调用。

promise对象还具有一个catch()方法,用于处理promise链中发生的错误。catch()方法接受一个回调函数,如果promise链中发生任何错误,则调用该函数。

现在,让我们继续学习如何在TypeScript中链式处理promise。

如何链式处理Promise

链式处理promise允许您按顺序或并行执行多个异步操作。当您需要依次执行多个异步任务或同时执行多个任务时,这将非常有帮助。例如,您可能需要异步获取数据,然后异步处理数据。

让我们看一个如何链式处理promise的示例:

// 链式 promises 的工作原理示例
// 第一个 promise
const promise1 = new Promise((resolve, reject) => {
  const functionOne: string = "This is the first promise function";
  setTimeout(() => {
    resolve(functionOne);
  }, 1000);
});

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

// 将第一个和第二个 promise 链接在一起
promise1
  .then(promise2)
  .then((result) => {
    console.log(result); // 输出: 这是第一个 promise 函数 + 这是第二个 promise 函数
  })
  .catch((error) => {
    console.error(error);
  });

在上面的示例中,我们有两个 promises: promise1promise2promise1 在 1 秒后解析为字符串 “这是第一个 promise 函数。” promise2 接受一个数字作为输入,并返回一个在 1 秒后解析为将输入数字与字符串 “这是第二个 promise 函数” 结合在一起的字符串的 promise。

我们使用 then 方法将两个 promises 链接在一起。将输出 promise1 作为输入传递给 promise2。最后,我们再次使用 then 方法将 promise2 的输出记录到控制台。如果 promise1promise2 中有任何一个拒绝,错误将被 catch 方法捕获。

恭喜!您已经学会了在 TypeScript 中创建和链式 promises。现在,让我们探讨在 TypeScript 中如何使用 Async/Await

如何在 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的async函数。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 等待响应,然后使用响应对象的 data 属性提取数据。最后,我们使用 console.log() 将数据记录到控制台中。发生的任何错误都会被捕获,并使用 console.error() 记录到控制台。

我们可以使用 Axios 实现这一点,因此您应该在控制台中看到相同的结果。

此图像显示了在控制台中使用 Axios 时的输出:

注意:在尝试上述代码之前,您需要使用 npm 或 yarn 安装 Axios。


npm install axios

yarn add axios

如果您对Axios不熟悉,您可以在这里了解更多

您可以看到我们使用了trycatch块来处理错误。trycatch块是TypeScript中管理错误的一种方法。所以,无论您像我们刚才那样进行API调用,都确保使用trycatch块来处理任何错误。

现在,让我们探索一下在TypeScript中更高级使用trycatch块的方法:

// 在typescript中如何使用async / await的示例3

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; // 返回recipes数组
  } catch (error) {
    console.error("Error fetching recipes:", error);
    if (error instanceof Error) {
      return error.message;
    }
    return "An unknown error occurred.";
  }
};

// 获取并记录recipes
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/awaittry/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

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

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的函数,它接受一个id和一个callback作为参数。这个callback是一个具有两个参数的函数:一个错误和一个用户。

fetchUserData函数使用id从JSONPlaceholder API端点检索用户数据。如果获取成功,它将创建一个User对象,并将其传递给回调函数以及一个空错误。如果在获取过程中出现错误,它将把错误发送到回调函数,并将用户设置为null。

要使用带有回调的fetchUserData函数,我们需要提供一个id和一个回调函数作为参数。回调函数会检查错误并在没有错误时记录用户数据。

下面的图片显示了API调用的输出:

如何负责任地使用回调函数

虽然回调函数在TypeScript中的异步编程中是基础,但需要谨慎管理以避免“回调地狱” – 那些金字塔形、深度嵌套的代码,使其难以阅读和维护。以下是如何有效使用回调函数:

  1. 避免深层嵌套

    • 通过将复杂操作拆分为命名函数来使代码结构扁平化

    • 对于复杂的异步工作流,请使用promises或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 事件)或创建具有简单异步需求的轻量级实用程序时,回调很有帮助。

在其他情况下,当您需要专注于编写具有清晰异步流程的可维护代码时,回调会带来麻烦,您应该选择 promises 或 async-await。例如,当您需要链接多个操作、处理复杂的错误传播、使用现代 API(如 Fetch API 或 FS Promises)或对并行执行使用promise.all()时。

从回调迁移到 promises 的示例:

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

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

// 使用async/await
try {
  const user = await fetchUserAsync(1);
} catch (error) {
  // 处理错误
}

异步模式的演变

模式 优点 缺点
回调函数 简单、通用 嵌套复杂性
Promise 可链式调用、更好的错误流程 需要.then()链
Async/Await 类似同步的可读性 需要转译

现代的TypeScript项目通常混合使用:回调函数处理事件驱动模式,而Promise/async-await处理复杂的异步逻辑。关键在于根据具体的使用情况选择合适的工具,同时保持代码清晰度。

结论

在本文中,我们学习了在TypeScript中处理异步代码的不同方式。我们了解了回调函数、Promise、async/await以及如何在TypeScript中使用它们。我们也了解了这个概念。

如果您想了解更多关于编程以及如何成为更好的软件工程师的知识,可以订阅我的YouTube频道CliffTech

感谢阅读我的文章。希望您喜欢。如果您有任何问题,请随时与我联系。

在社交媒体上关注我: