FORMA

异步编程与事件循环

JavaScript 在单线程上通过事件循环调度异步任务。前置:语言基础

一、事件循环(Event Loop)

JavaScript 是单线程的,事件循环是其实现异步非阻塞 I/O 的核心机制。

1. 宏任务(MacroTask)与微任务(MicroTask)

浏览器(HTML 规范中的 task)常见划分:

  • 宏任务(task)setTimeout / setIntervalMessageChannel、I/O 回调、用户事件等。
  • 微任务(microtask)Promise 回调、queueMicrotask()MutationObserver 等。

Node.js 另有 process.nextTick(在当前阶段之后、微任务之前执行)与 setImmediate(check 阶段),与浏览器队列细节不同,勿混为一谈。

requestAnimationFrame 在浏览器中用于下一帧绘制前回调,不属于上述微任务队列,通常在本轮微任务清空后、渲染前执行。

2. 执行顺序

  1. 执行一个宏任务(从宏任务队列中取出)
  2. 执行当前宏任务关联的所有微任务(清空微任务队列)
  3. 必要时进行 UI 渲染(浏览器)
  4. 进入下一轮循环,执行下一个宏任务
js
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// 输出: 1, 4, 3, 2

3. 常见误区

  • setTimeout(fn, 0) 并不立即执行,它需要等待当前宏任务和微任务全部完成。
  • 微任务中可以继续添加微任务,会一直执行直到微任务队列为空(可能导致“卡死”)。

二、回调地狱与解救

1. 回调地狱(Callback Hell)

多个异步操作层层嵌套,导致代码难以阅读和维护。

js
getData(function (a) {
    getMoreData(a, function (b) {
        getMoreMoreData(b, function (c) {
            console.log(c);
        });
    });
});

2. Promise 链式调用

Promise 是一个代表异步操作最终完成(或失败)的对象。通过 .then().catch() 实现链式调用,避免嵌套。

js
getData()
    .then(a => getMoreData(a))
    .then(b => getMoreMoreData(b))
    .then(c => console.log(c))
    .catch(err => console.error(err));

3. Promise 静态方法

  • Promise.all(iterable):所有成功才成功,任一失败立即失败(返回失败原因)。
  • Promise.allSettled(iterable):等待所有完成(无论成功或失败),返回每个结果的状态和值。
  • Promise.any(iterable):任意一个成功即成功,全部失败才失败(返回 AggregateError)。
  • Promise.race(iterable):返回最先敲定(成功或失败)的结果。

4. 错误冒泡

在 Promise 链中,catch 可以捕获链条中任意位置抛出的错误(同步或异步)。若没有 catch,错误会被静默吞没(现代浏览器/Node.js 会触发 unhandledrejection 事件)。

三、async/await

async/await 是 Promise 的语法糖,使异步代码看起来像同步代码,更易于阅读和调试。

1. 原理:生成器(Generator)+ 自动执行器

async 函数内部会被编译成一个生成器函数(Generator),配合类似 co 库的自动执行器,每次 await 对应一次 yield,自动处理 next() 调用和 Promise 解析。

2. 基本用法

js
async function fetchData() {
    try {
        const a = await getData();
        const b = await getMoreData(a);
        return b;
    } catch (err) {
        console.error(err);
    }
}

3. 错误处理

  • try/catch:可以捕获 await 抛出的 rejected Promise,也捕获同步错误。
  • .catch():在 await 后直接链式调用 .catch()
js
const data = await fetch(url)
    .then(res => res.json())
    .catch(err => null);

注意:async 函数总是返回一个 Promise,如果函数内没有 return,则返回 Promise.resolve(undefined)

4. 顶层 await(ES2022)

在模块中可以直接使用 await 而不需要包裹 async 函数,适用于模块初始化依赖异步操作。

四、异步迭代器(for await...of)与异步生成器

1. 异步迭代器(Async Iterator)

一个对象实现了 Symbol.asyncIterator 方法,该方法返回一个对象,其 next() 方法返回 Promise<{value, done}>。可以使用 for await...of 循环消费。

js
const asyncIterable = {
    [Symbol.asyncIterator]() {
        let i = 0;
        return {
            next() {
                if (i < 3) {
                    return Promise.resolve({ value: i++, done: false });
                }
                return Promise.resolve({ done: true });
            },
        };
    },
};

(async () => {
    for await (const num of asyncIterable) {
        console.log(num); // 0,1,2
    }
})();

2. 异步生成器(Async Generator)

使用 async function* 定义,可以 yield Promise 或直接 yield 值,for await...of 会自动等待每个 yield 的 Promise 解析。

js
async function* generateSequence() {
    for (let i = 0; i < 3; i++) {
        await new Promise(r => setTimeout(r, 100)); // 模拟异步
        yield i;
    }
}

(async () => {
    for await (const val of generateSequence()) {
        console.log(val); // 0,1,2 (每个间隔 100ms)
    }
})();

3. 典型应用

  • 处理分页 API 的流式数据。
  • 读取大文件(Node.js 流)。
  • 生成无限序列(如 WebSocket 消息流)。

五、AbortController 中止异步操作

AbortController 提供了一个标准方式来取消一个或多个异步操作(例如 fetch 请求、事件监听等)。核心思想:通过信号(signal)传递,调用 abort() 方法触发中断。

1. 基本用法

js
const controller = new AbortController();
const { signal } = controller;

fetch("/api/data", { signal })
    .then(res => res.json())
    .catch(err => {
        if (err.name === "AbortError") {
            console.log("Fetch aborted");
        } else {
            console.error("Other error:", err);
        }
    });

// 取消请求
controller.abort();

2. 中止多个请求

同一个 signal 可以传入多个 fetch,调用一次 abort() 会取消所有相关请求。

3. 中止其他异步操作(如事件监听)

任何支持 signal 的 API 都可以使用,例如 addEventListenerAbortSignal

js
const controller = new AbortController();
element.addEventListener("click", handler, { signal: controller.signal });
// 后续调用 controller.abort() 会移除该监听器

4. 手动实现可中止的异步任务

可以监听 signalabort 事件,自定义清理逻辑。

js
function wait(ms, signal) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(resolve, ms);
        signal?.addEventListener("abort", () => {
            clearTimeout(timer);
            reject(new DOMException("Aborted", "AbortError"));
        });
    });
}

const controller = new AbortController();
wait(5000, controller.signal).catch(err => console.log(err.name));
controller.abort(); // 立即中止

5. 注意要点

  • AbortController 只能触发一次 abort,不可复用。
  • 浏览器原生支持 fetchaddEventListener,Node.js 从 v15+ 开始支持 AbortController 用于 fshttp 等模块。

总结

概念核心要点
事件循环宏任务与微任务的优先级模型,决定了异步代码的执行时机
Promise链式调用、静态方法(all/allSettled/any/race)、错误冒泡
async/await基于生成器 + 自动执行器的语法糖,配合 try/catch 或 .catch 处理错误
异步迭代器/生成器for await...of 消费异步数据流,async function* 生产异步序列
AbortController通过 signal 取消 fetch、事件监听等,可自定义中止逻辑

掌握这些内容,就能高效处理 JavaScript 中几乎所有的异步场景,写出清晰、健壮且可取消的异步代码。

参考文献

以下链接在编写时均可正常访问:

资料说明
MDN:并发模型与事件循环事件循环
MDN:PromisePromise API
MDN:async functionasync/await
MDN:AbortController中止异步操作
HTML:event loop浏览器事件循环(英文)

Series

js

7 / 14