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. 事件循环
事件循环不断检查调用栈是否为空,如果为空,就从任务队列中取出任务执行。
可以简单理解为:
- 同步代码先执行
- 异步任务先注册
- 完成后的回调排队
- 栈空了再执行回调
四、回调函数(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`:进行中
- `fulfilled`:已成功
- `rejected`:已失败
状态一旦从 `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);
});
- `then` 处理成功结果
- `catch` 处理失败结果
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 的语法糖
- `await` 后面通常跟一个 Promise
- `async` 函数本质上总是返回 Promise
- 没有 Promise,就没有现代 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("最终失败");
}
十四、常见误区
- 误区一:`async` 函数返回普通值就不是 Promise
实际上仍然是 Promise。
- 误区二:`await` 可以阻塞整个程序
实际上它只是暂停当前 async 函数内部的执行。
- 误区三:用了 `await` 就一定更快
如果写成串行反而可能更慢。
- 误区四:Promise 和回调完全无关
实际上 Promise 内部很多时候仍然基于回调触发结果。
十五、总结
TypeScript 的异步编程,核心可以归纳为以下几个层次:
- 理解同步与异步的区别
- 掌握事件循环的基本原理
- 了解回调函数及其缺陷
- 熟练使用 Promise 处理异步结果
- 使用 `async/await` 编写更清晰的异步代码
- 学会并发控制与错误处理
- 利用 TypeScript 类型系统为异步函数提供更安全的约束
如果把异步知识体系看作一个成长路径,那么可以概括为:
- 回调函数是起点
- Promise 是核心
- async/await 是实践中的主流写法
- 类型系统让异步代码更加可靠、可维护
在实际开发中,建议优先使用 `async/await` 编写业务逻辑,同时理解 Promise 的底层思想;在需要并发时配合 `Promise.all`、`Promise.allSettled` 等 API 使用。只有把“运行机制 + Promise + async/await + 类型约束”四者结合起来,才能真正掌握 TypeScript 异步编程。
如果你愿意,我还可以继续为你输出一份:
- 更适合直接粘贴到 DokuWiki 的排版优化版
- 带面试题的 TypeScript 异步专题版
- 包含微任务、宏任务、事件循环深入分析的进阶版
你只要回复一句:继续扩展 即可。