目录

TypeScript 中异步编程知识点详解

TypeScript 中的异步编程,是前端与 Node.js 开发中极其重要的一部分。无论是发送网络请求、读取文件、执行定时任务,还是数据库访问,都离不开“异步”机制。下面我将使用 DokuWiki 语法,系统讲解 TypeScript 中与异步相关的核心知识点。

一、什么是异步

在理解 TypeScript 异步之前,首先要明白“同步”和“异步”的区别。

1. 同步

同步代码会按照从上到下的顺序依次执行,前面的任务没有完成,后面的任务就不能开始。

console.log("任务1");
console.log("任务2");
console.log("任务3");

执行结果:

任务1
任务2
任务3

这种执行方式简单直观,但如果某个任务耗时很长,例如网络请求或者文件读取,那么后续任务就会被阻塞。

2. 异步

异步代码的特点是:某个耗时任务启动后,不必等待它完成,程序可以继续执行后面的代码,等任务完成后再处理结果。

console.log("开始");
 
setTimeout(() => {
  console.log("异步任务完成");
}, 1000);
 
console.log("结束");

执行顺序通常为:

开始
结束
异步任务完成

这说明 `setTimeout` 中的逻辑并不会阻塞后续代码执行。

二、为什么 TypeScript 需要异步

TypeScript 是 JavaScript 的超集,因此它的异步模型本质上继承自 JavaScript。之所以需要异步,主要有以下原因:

例如,浏览器中如果所有请求都采用同步方式,那么页面会卡住,用户体验会非常差。

三、JavaScript 事件循环与异步基础

虽然题目是 TypeScript,但 TypeScript 的异步建立在 JavaScript 运行机制之上,所以必须了解事件循环(Event Loop)。

1. 调用栈

调用栈负责执行当前同步代码。函数调用时入栈,执行完成后出栈。

2. Web APIs / Node APIs

像 `setTimeout`、HTTP 请求、文件系统操作等,不是直接由 JavaScript 引擎完成,而是交给宿主环境处理。

3. 回调队列

异步任务完成后,对应的回调函数会进入任务队列,等待调用栈空闲后执行。

4. 事件循环

事件循环不断检查调用栈是否为空,如果为空,就从任务队列中取出任务执行。

可以简单理解为:

  1. 同步代码先执行
  2. 异步任务先注册
  3. 完成后的回调排队
  4. 栈空了再执行回调

四、回调函数(Callback)

在 Promise 出现之前,JavaScript 主要通过回调函数处理异步逻辑。

1. 基本写法

function fetchData(callback: (data: string) => void): void {
  setTimeout(() => {
    callback("获取到的数据");
  }, 1000);
}
 
fetchData((data) => {
  console.log(data);
});

这里 `callback` 是一个函数参数,异步任务完成后被调用。

2. 回调的优点

3. 回调的问题

当多个异步任务相互依赖时,就容易出现“回调地狱”。

setTimeout(() => {
  console.log("任务1完成");
  setTimeout(() => {
    console.log("任务2完成");
    setTimeout(() => {
      console.log("任务3完成");
    }, 1000);
  }, 1000);
}, 1000);

这种嵌套层级很深,代码难以维护、难以扩展、错误处理也不统一。

五、Promise:异步编程的重要改进

为了解决回调函数的问题,ES6 引入了 `Promise`。TypeScript 对 Promise 提供了良好的类型支持。

1. Promise 的概念

`Promise` 可以理解为“一个未来才会完成的结果”。

Promise 有三种状态:

状态一旦从 `pending` 变为成功或失败,就不能再改变。

2. 创建 Promise

const p = new Promise<string>((resolve, reject) => {
  const success = true;
 
  setTimeout(() => {
    if (success) {
      resolve("操作成功");
    } else {
      reject("操作失败");
    }
  }, 1000);
});

这里的 `Promise<string>` 表示最终成功时返回的是字符串类型。

3. 使用 then 和 catch

p.then((result) => {
  console.log("成功:", result);
}).catch((error) => {
  console.error("失败:", error);
});

4. 链式调用

Promise 最大的优势之一就是链式调用。

function step1(): Promise<number> {
  return Promise.resolve(1);
}
 
function step2(num: number): Promise<number> {
  return Promise.resolve(num + 1);
}
 
function step3(num: number): Promise<number> {
  return Promise.resolve(num + 1);
}
 
step1()
  .then((res) => step2(res))
  .then((res) => step3(res))
  .then((res) => {
    console.log("最终结果:", res);
  })
  .catch((err) => {
    console.error(err);
  });

这种方式比多层回调更加清晰。

六、TypeScript 中 Promise 的类型系统

TypeScript 相比 JavaScript 的优势,在于可以明确异步返回值类型。

1. 指定返回类型

function getUserName(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("Alice");
    }, 1000);
  });
}

这里函数返回的不是 `string`,而是 `Promise<string>`。这表示“未来会得到一个字符串”。

2. 返回对象类型

type User = {
  id: number;
  name: string;
};
 
function getUser(): Promise<User> {
  return Promise.resolve({
    id: 1,
    name: "Tom"
  });
}

3. Promise<void>

如果异步函数没有明确返回值,可使用 `Promise<void>`。

function logAsync(): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("记录完成");
      resolve();
    }, 500);
  });
}

4. Promise<never>

某些函数始终抛出错误或永远不会正常完成,可以涉及 `Promise<never>`,不过实际业务中使用较少。

七、async / await:更现代的异步写法

`async / await` 是建立在 Promise 之上的语法糖,让异步代码看起来更像同步代码。

1. async 函数

只要在函数前加上 `async`,该函数就会返回一个 Promise。

async function hello(): Promise<string> {
  return "Hello TypeScript";
}

虽然直接返回的是字符串,但实际上会被包装成 `Promise<string>`。

2. await 的作用

`await` 用于等待 Promise 完成,并获取其结果。

function getData(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("数据加载完成");
    }, 1000);
  });
}
 
async function main(): Promise<void> {
  const data = await getData();
  console.log(data);
}
 
main();

3. async / await 的优势

4. 错误处理

使用 `try…catch` 捕获异步错误是 async/await 的常见方式。

async function requestData(): Promise<void> {
  try {
    const result = await Promise.reject("请求失败");
    console.log(result);
  } catch (error) {
    console.error("捕获到错误:", error);
  }
}

八、Promise 与 async/await 的关系

很多初学者容易误以为 `async/await` 是另一套独立机制,实际上不是。

它们的关系可以总结为:

也就是说,学习 `async/await` 前,必须先掌握 Promise。

九、常用 Promise API

1. Promise.resolve

快速创建一个成功状态的 Promise。

const p = Promise.resolve(123);

2. Promise.reject

快速创建一个失败状态的 Promise。

const p = Promise.reject("出错了");

3. Promise.all

用于并发执行多个异步任务,全部成功才算成功。

async function demo(): Promise<void> {
  const results = await Promise.all([
    Promise.resolve("A"),
    Promise.resolve("B"),
    Promise.resolve("C")
  ]);
 
  console.log(results);
}

返回结果是数组:

["A", "B", "C"]

如果其中一个失败,则整体失败。

4. Promise.allSettled

无论成功还是失败,都会等待全部任务完成。

async function demo(): Promise<void> {
  const results = await Promise.allSettled([
    Promise.resolve("成功1"),
    Promise.reject("失败1"),
    Promise.resolve("成功2")
  ]);
 
  console.log(results);
}

适合需要统计全部执行结果的场景。

5. Promise.race

谁先完成,就返回谁的结果。

async function demo(): Promise<void> {
  const result = await Promise.race([
    new Promise((resolve) => setTimeout(() => resolve("快任务"), 500)),
    new Promise((resolve) => setTimeout(() => resolve("慢任务"), 1000))
  ]);
 
  console.log(result);
}

6. Promise.any

返回第一个成功的 Promise,如果全失败才报错。

这在需要“多个备用接口取最快成功结果”时非常有用。

十、并发与串行

异步并不等于并发。理解串行与并发很重要。

1. 串行执行

前一个完成后再执行下一个。

async function serial(): Promise<void> {
  const a = await Promise.resolve("A");
  const b = await Promise.resolve("B");
  const c = await Promise.resolve("C");
 
  console.log(a, b, c);
}

2. 并发执行

多个任务同时开始,再统一等待结果。

async function parallel(): Promise<void> {
  const p1 = Promise.resolve("A");
  const p2 = Promise.resolve("B");
  const p3 = Promise.resolve("C");
 
  const results = await Promise.all([p1, p2, p3]);
  console.log(results);
}

在彼此无依赖的情况下,并发通常效率更高。

十一、异步错误处理最佳实践

1. 不要忽略 Promise 错误

async function badExample() {
  Promise.reject("错误");
}

这样可能导致未处理的 Promise 异常。

2. 使用 try...catch

async function goodExample(): Promise<void> {
  try {
    await Promise.reject("错误");
  } catch (e) {
    console.error(e);
  }
}

3. 合理定义错误类型

在 TypeScript 中,`catch` 捕获的错误通常建议先做类型收窄。

try {
  throw new Error("出错");
} catch (e) {
  if (e instanceof Error) {
    console.log(e.message);
  }
}

十二、异步函数的类型声明

TypeScript 中可以为异步函数、回调函数、接口方法声明明确类型。

1. 函数类型

type AsyncFunc = (id: number) => Promise<string>;
 
const fetchName: AsyncFunc = async (id) => {
  return `name-${id}`;
};

2. 接口中的异步方法

interface UserService {
  getUser(id: number): Promise<{ id: number; name: string }>;
}

3. 泛型异步函数

async function wrap<T>(value: T): Promise<T> {
  return value;
}

泛型让异步函数更具复用性。

十三、实际开发中的常见场景

1. 请求接口

type Post = {
  id: number;
  title: string;
};
 
async function fetchPosts(): Promise<Post[]> {
  const response = await fetch("/api/posts");
  const data = await response.json();
  return data as Post[];
}

2. 延时工具函数

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
 
async function demo(): Promise<void> {
  console.log("开始");
  await sleep(1000);
  console.log("1秒后执行");
}

3. 重试机制

async function retry(fn: () => Promise<string>, count: number): Promise<string> {
  for (let i = 0; i < count; i++) {
    try {
      return await fn();
    } catch (e) {
      if (i === count - 1) throw e;
    }
  }
  throw new Error("最终失败");
}

十四、常见误区

实际上仍然是 Promise。

实际上它只是暂停当前 async 函数内部的执行。

如果写成串行反而可能更慢。

实际上 Promise 内部很多时候仍然基于回调触发结果。

十五、总结

TypeScript 的异步编程,核心可以归纳为以下几个层次:

如果把异步知识体系看作一个成长路径,那么可以概括为:

  1. 回调函数是起点
  2. Promise 是核心
  3. async/await 是实践中的主流写法
  4. 类型系统让异步代码更加可靠、可维护

在实际开发中,建议优先使用 `async/await` 编写业务逻辑,同时理解 Promise 的底层思想;在需要并发时配合 `Promise.all`、`Promise.allSettled` 等 API 使用。只有把“运行机制 + Promise + async/await + 类型约束”四者结合起来,才能真正掌握 TypeScript 异步编程。

如果你愿意,我还可以继续为你输出一份:

你只要回复一句:继续扩展 即可。