TypeScript 中回调、Promises 和 Async Await 的比较

2025-05-24

TypeScript 中回调、Promises 和 Async Await 的比较

回调、Promise 和 async/await 之间有何区别?本文展示了使用这三种技术在相同场景下的应用,以便您了解它们之间的差异,并选择最符合您需求的方案。

代码使用TypeScript,但可以轻松地适应 JavaScript。

在我的课程“在 Pluralsight 上创建异步 TypeScript 代码”中了解有关此代码的更多信息。

本文展示了三种不同的技术来获取英雄的对象图以及该英雄的命令和帐户代表。

这些示例的场景是,有一组英雄。每个英雄都需要购物,所以他们会下订单。每个英雄都有一个专门负责其订单的客服代表。英雄就像顾客一样,希望这能有所帮助😄

回调

编写回调函数时,我们最终会得到一系列嵌套调用。查看下面的代码很容易发现这一点,因为它们总是倾向于向右偏移。这种偏移也被称为“毁灭金字塔”。

下面的代码通过英雄的邮箱获取英雄。然后,它获取该英雄的订单,并将它们合并到英雄对象中。接着,它获取该英雄的账户仓库,并将这些数据合并到英雄对象中。最后,它返回该英雄及其所有订单和账户代表数据。

const getHeroTreeCallback = function(
  email: string,
  callback: Callback<Hero>,
  callbackError?: CallbackError,
) {
  getHeroCallback(
    email,
    hero => {
      getOrdersCallback(
        hero.id,
        orders => {
          hero.orders = orders;
          getAccountRepCallback(
            hero.id,
            accountRep => {
              hero.accountRep = accountRep;
              callback(hero);
            },
            error => callbackError(error),
          );
        },
        error => callbackError(error),
      );
    },
    error => callbackError(error),
  );
};

单独回调

getHeroTeeCallback函数调用嵌套函数。您可以在以下代码示例中看到这些。

这里有三个函数。每个函数分别获取 Hero、Hero 的订单以及 Hero 的账户代表。请注意,每个函数都遵循使用 axios 通过 http 获取数据的模式,并根据代码是否成功或遇到错误来调用callback或函数。callbackError

const getHeroCallback = function(
  email: string,
  callback: Callback<Hero>,
  callbackError?: CallbackError,
) {
  axios
    .get<Hero[]>(`${apiUrl}/heroes?email=${email}`)
    .then((response: AxiosResponse<Hero[]>) => {
      const data = parseList<Hero>(response);
      const hero = data[0];
      callback(hero);
    })
    .catch((error: AxiosError) => {
      console.error(`Developer Error: Async Data Error: ${error.message}`);
      callbackError(`Oh no! We're unable to fetch the Hero`);
    });
};

const getOrdersCallback = function(
  heroId: number,
  callback: Callback<Order[]>,
  callbackError?: CallbackError,
) {
  const url = heroId ? `${apiUrl}/orders/${heroId}` : `${apiUrl}/orders`;
  axios
    .get(url)
    .then((response: AxiosResponse<Order[]>) => {
      const orders = parseList<Order>(response);
      callback(orders);
    })
    .catch((error: AxiosError) => {
      console.error(`Developer Error: Async Data Error: ${error.message}`);
      callbackError(`Oh no! We're unable to fetch the Orders`);
    });
};

const getAccountRepCallback = function(
  heroId: number,
  callback: Callback<AccountRepresentative>,
  callbackError?: CallbackError,
) {
  const url = `${apiUrl}/accountreps/${heroId}`;
  axios
    .get(url)
    .then((response: AxiosResponse<AccountRepresentative>) => {
      const list = parseList<AccountRepresentative>(response);
      const accountRep = list[0];
      callback(accountRep);
    })
    .catch((error: AxiosError) => {
      console.error(`Developer Error: Async Data Error: ${error.message}`);
      callbackError(`Oh no! We're unable to fetch the Account Rep`);
    });
};

承诺

Promise 确实有一些向右缩进,就像回调一样。不过,缩进程度通常不会那么极端。调用该 Promise 来获取 Hero,并then使用 来同时检索订单和账户代表Promise.all

这与每次调用一次的 allback 技术不同。Promise.all它允许您获取英雄数据并使用它进行两次调用:一次用于订单,一次用于客户代表。当两个调用都返回响应后,代码将进入下一个调用then

最后一步是将订单和账户 repo 数据合并到 Hero 中。

还要注意,嵌套函数位于函数内部getHeroTreePromise。这允许这些函数访问hero外部函数中的变量。否则,你可能需要将英雄传递给其他函数。我更喜欢这种闭包技术,因为它为这些函数提供了它们应该在何处(针对英雄)工作的上下文。

const getHeroTreePromise = function(searchEmail: string) {
  let hero: Hero;

  // Level 1 - Get the hero record
  return (
    getHeroPromise(searchEmail)
      // Level 2 - Set the hero, and pass it on
      .then((h: Hero) => {
        hero = h;
        return h;
      })
      // Level 3 - Get the orders and account reps
      .then((hero: Hero) => Promise.all([getOrders(hero), getAccountRep(hero)]))
      // Extract the orders and account reps and put them on their respective Hero objects
      .then((result: [Order[], AccountRepresentative]) => mergeData(result))
  );

  function getOrders(h: Hero): Promise<Order[]> {
    hero = h;
    return h ? getOrdersPromise(h.id) : undefined;
  }

  function getAccountRep(h: Hero): Promise<AccountRepresentative> {
    hero = h;
    return h ? getAccountRepPromise(h.id) : undefined;
  }

  function mergeData(result: [Order[], AccountRepresentative]): Hero {
    const [orders, accountRep] = result;
    if (orders) {
      hero.orders = orders;
    }
    if (accountRep) {
      hero.accountRep = accountRep;
    }
    return hero;
  }
};

异步等待

异步等待技术获取相同的数据,但遵循更“先做这个,再做那个”的流程。代码逐行流动,就像同步代码一样。

首先,你获取英雄。然后,你获取订单和客户代表。注意,你可以将Promise.allcombined 与 async await 结合使用。这非常有用,因为它允许你同时进行两个调用,但仍然“等待”它们的响应。然后,这些响应会被合并到hero对象中。

我觉得这段代码最简洁。行数更少,而且更容易阅读。

const getHeroTreeAsync = async function(email: string) {
  const hero = await getHeroAsync(email);
  if (!hero) return;

  const [orders, accountRep] = await Promise.all([
    getOrdersAsync(hero.id),
    getAccountRepAsync(hero.id),
  ]);
  hero.orders = orders;
  hero.accountRep = accountRep;
  return hero;
};

嵌套异步函数

async await 函数调用的函数getHeroTreeAsync如下所示。这里它们使用 axiosasyncawait关键字。检索数据并返回。

const getHeroAsync = async function(email: string) {
  try {
    const response = await axios.get(`${apiUrl}/heroes?email=${email}`);
    const data = parseList<Hero>(response);
    const hero = data[0];
    return hero;
  } catch (error) {
    handleAxiosErrors(error, 'Hero');
  }
};

const getOrdersAsync = async function(heroId: number) {
  try {
    const response = await axios.get(`${apiUrl}/orders/${heroId}`);
    const data = parseList<Order>(response);
    return data;
  } catch (error) {
    handleAxiosErrors(error, 'Orders');
  }
};

const getAccountRepAsync = async function(heroId: number) {
  try {
    const response = await axios.get(`${apiUrl}/accountreps/${heroId}`);
    const data = parseList<AccountRepresentative>(response);
    return data[0];
  } catch (error) {
    handleAxiosErrors(error, 'Account Rep');
  }
};

资源

您可以从以下资源中了解有关这些技术的更多信息:

文章来源:https://dev.to/azure/comparing-callbacks-promises-and-async-await-in-typescript-4noi
PREV
将 API 从 Express 迁移到无服务器功能时,如何构建代码?
NEXT
构建 Visual Studio Code 扩展 Visual Studio Code 扩展开发