编程语言

TypeScript教程(三)异步编程与模块化

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. 练习

  1. 用 mapLimit + retry 实现:并发 3 个,检查一批 URL 的状态码,输出表格。
  2. 写一个 withTimeout<T>(p: Promise<T>, ms: number),超时抛错(提示:Promise.race)。
  3. 把第 01 篇的 CSV 汇总改成异步、并支持一次处理一个目录下的多个 CSV。

文章评论 (0)