TypeScript 03:异步编程与模块化
1. 事件循环:和 Python asyncio 的区别
- JS/Node 是单线程 + 事件循环,所有 I/O 天然异步(不像 Python 需要专门的 aiohttp 等库)。
Promise≈ Python 的Future/Task,≈ C# 的Task<T>。async/await语法与 C# 几乎一致。- 不需要
asyncio.run(),Node 顶层可直接await(ES Module 中)。
2. Promise 与 async/await
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function fetchPost(id: number): Promise<{ id: number; title: string }> {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
async function main() {
try {
const post = await fetchPost(1);
console.log(post.title);
} catch (err) {
console.error("failed:", (err as Error).message);
} finally {
console.log("done");
}
}
main();
Node 18+ 内置
fetch,相当于 Python 的requests,但它是异步的。
3. 并发控制 —— 应用场景
| 需求 | Python asyncio | TypeScript |
|---|---|---|
| 并发执行,全部成功 | asyncio.gather(*tasks) |
Promise.all(promises) |
| 并发执行,收集成功/失败 | gather(..., return_exceptions=True) |
Promise.allSettled(promises) |
| 取最快的一个 | asyncio.wait(FIRST_COMPLETED) |
Promise.race / Promise.any |
| 超时 | asyncio.wait_for |
AbortSignal.timeout(ms) |
// 批量拉取文章,单个失败不影响整体
const ids = [1, 2, 3, 999999];
const results = await Promise.allSettled(ids.map(fetchPost));
results.forEach((r, i) => {
if (r.status === "fulfilled") console.log(ids[i], r.value.title);
else console.warn(ids[i], "失败:", r.reason.message);
});
// 带超时的请求
const res = await fetch("https://example.com", { signal: AbortSignal.timeout(3000) });
限制并发数(批量调用第三方 API 时常用)
async function mapLimit<T, R>(
items: T[], limit: number, fn: (item: T) => Promise<R>,
): Promise<R[]> {
const results: R[] = new Array(items.length);
let index = 0;
async function worker() {
while (index < items.length) {
const i = index++;
results[i] = await fn(items[i]);
}
}
await Promise.all(Array.from({ length: limit }, worker));
return results;
}
const posts = await mapLimit([1, 2, 3, 4, 5, 6], 2, fetchPost);
重试(带指数退避)
async function retry<T>(fn: () => Promise<T>, times = 3, delay = 500): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await fn();
} catch (e) {
if (attempt >= times) throw e;
await sleep(delay * 2 ** (attempt - 1));
}
}
}
const post = await retry(() => fetchPost(1));
4. 常见坑
// 坑1:forEach 中 await 不会等待
ids.forEach(async id => await fetchPost(id)); // ❌ 外层不会等
for (const id of ids) await fetchPost(id); // ✅ 顺序执行
await Promise.all(ids.map(fetchPost)); // ✅ 并发执行
// 坑2:忘记 await,拿到的是 Promise 对象
const p = fetchPost(1); // Promise<...>,不是数据
// 坑3:catch 中的 err 类型是 unknown
try { } catch (err) {
if (err instanceof Error) console.log(err.message);
}
5. 模块化(ES Module)
package.json 中设置 "type": "module"。
// utils/math.ts
export function add(a: number, b: number) { return a + b; }
export const PI = 3.14;
export default class Calculator { }
// main.ts
import Calculator, { add, PI } from "./utils/math.js"; // 注意:ESM 下写 .js 后缀
import * as math from "./utils/math.js";
import type { Post } from "./types.js"; // 只导入类型
| Python | TypeScript |
|---|---|
from a import b |
import { b } from "./a.js" |
import a |
import * as a from "./a.js" |
__init__.py 聚合导出 |
index.ts 中 export * from "./x.js" |
if __name__ == "__main__" |
通常单独一个入口文件main.ts |
推荐的 tsconfig.json(Node 项目):
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"outDir": "dist",
"rootDir": "src",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
6. 应用场景:异步文件处理 + 流
import { readdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
// 统计目录下所有 .md 文件的字数(类似给博客文章做统计)
async function countWords(dir: string) {
const files = (await readdir(dir)).filter(f => f.endsWith(".md"));
const stats = await Promise.all(files.map(async f => {
const text = await readFile(path.join(dir, f), "utf-8");
return { file: f, words: text.split(/\s+/).filter(Boolean).length };
}));
await writeFile("stats.json", JSON.stringify(stats, null, 2));
return stats;
}
console.table(await countWords("./docs"));
大文件逐行读取(≈ Python for line in f):
import { createReadStream } from "node:fs";
import readline from "node:readline";
const rl = readline.createInterface({ input: createReadStream("access.log") });
let errors = 0;
for await (const line of rl) {
if (line.includes(" 500 ")) errors++;
}
console.log("5xx count:", errors);
7. 练习
- 用
mapLimit+retry实现:并发 3 个,检查一批 URL 的状态码,输出表格。 - 写一个
withTimeout<T>(p: Promise<T>, ms: number),超时抛错(提示:Promise.race)。 - 把第 01 篇的 CSV 汇总改成异步、并支持一次处理一个目录下的多个 CSV。
文章评论 (0)