JS随笔:异步编程与事件循环
本篇是「JS随笔」系列中的异步与事件循环篇,聚焦 JavaScript 的单线程与事件循环(Event Loop)机制,以及 Promise 与 async/await 的核心用法与实践,帮助在不阻塞主线程的同时完成高并发异步任务。
原文地址
单线程模型
JavaScript 采用单线程模型,意味着任一时刻仅有一个主线程执行代码。
- 优势:简化并发与同步问题;避免共享数据的竞态
- 挑战:密集计算可能阻塞 UI;需借助异步与任务分解避免卡顿
Event Loop 工作流
事件循环允许 JS 在单线程环境中处理异步:
- 调用栈(Call Stack):同步任务在栈中执行
- 事件队列(Event Queue):异步任务的回调入队待执行
- 轮询(Loop):栈空时从队列取出任务入栈执行
- 宏/微任务队列:
- 宏任务:
script、setTimeout、setInterval、I/O、UI渲染 - 微任务:
Promise回调、MutationObserver
- 宏任务:
- 执行顺序:优先清空微任务队列,再处理下一个宏任务
- 浏览器 vs Node.js:两者队列实现细节存在差异
示例
console.log('Script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('Script end');
执行顺序:
Script start
Script end
promise1
promise2
setTimeout
Promise
Promise 表示一个尚未完成但预期未来会完成的异步操作。
const myPromise = new Promise((resolve, reject) => {
// 异步操作
resolve('value');
});
myPromise
.then(value => {
// 处理返回值
})
.catch(error => {
// 处理错误
});
链式调用
then() 返回新的 Promise,便于链式组织多个异步步骤。
需要特别注意:
then/catch中抛出的异常会进入后续的catch,不要在每一层都捕获并“吃掉”错误- 若链尾缺少
catch,在部分环境中会出现“未处理的 Promise 拒绝”,影响错误监控
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
// 使用数据
})
.catch(error => {
// 错误处理
});
静态方法
Promise.all([p1, p2, p3]).then(results => { /* 所有成功 */ });
Promise.race([p1, p2]).then(result => { /* 最先完成的决定结果 */ });
在实践中可以遵循:
- 需要“全部成功才继续”时使用
Promise.all,并注意其中任一失败都会导致整体失败 - 需要“收集所有结果”时使用
Promise.allSettled,避免因为单个错误中断整体流程 - 与 UI 交互搭配时,应为长链路 Promise 增加超时与取消能力
async/await
async 将函数声明为异步,await 等待 Promise 解决,以同步风格书写异步逻辑。
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// 使用数据
} catch (error) {
// 错误处理
}
}
实战策略
- 优先以
await顺序化关键依赖链,减少嵌套 - 并行独立任务:
await Promise.all([...]) - 控制并发:构建限流器或使用队列,避免过度并发打爆资源
- 兜底与重试:
try/catch+ 指数退避重试 - 可取消任务:封装
AbortController,避免悬空回调占用资源 - 微任务 vs 宏任务:关注 UI 时机与绘制顺序,避免掉帧
与模块的协作
动态 import() 可在需求时加载模块,配合路由与组件懒加载提升首屏性能;事件循环与网络调度共同决定资源拉取时机。
宏任务与微任务细节
- 宏任务示例:
script、setTimeout、setInterval、I/O、UI渲染 - 微任务示例:
Promise.then/catch/finally、MutationObserver、部分平台的queueMicrotask - 调度顺序:每个宏任务结束后清空微任务队列,再进入下一个宏任务
- 实践建议:将轻量、依赖当前帧状态的逻辑放入微任务;将耗时逻辑放入宏任务或 Worker
queueMicrotask(() => {
// 在当前宏任务末尾、绘制前执行
});
setTimeout(() => {
// 下一个宏任务
}, 0);
浏览器 vs Node.js 差异
- 浏览器:事件循环与渲染管线协作;微任务在绘制前执行,影响 UI 时机
- Node.js:存在更细的阶段划分(timers、pending callbacks、poll、check、close callbacks),
process.nextTick与Promise微任务的执行顺序有差异
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
并发控制模式
- 限流器(Semaphore):限制同时进行的任务数量
- 批处理(Batching):将零散任务聚合成批次执行
- 背压(Backpressure):在生产者-消费者模型中控制生产速度
function pLimit(max) {
const queue = [];
let active = 0;
const next = () => {
if (active >= max || queue.length === 0) return;
active++;
const { fn, resolve, reject } = queue.shift();
Promise.resolve()
.then(fn)
.then(v => resolve(v))
.catch(reject)
.finally(() => { active--; next(); });
};
return (fn) => new Promise((resolve, reject) => { queue.push({ fn, resolve, reject }); next(); });
}
取消与超时
- AbortController:为
fetch与自定义异步任务提供取消能力 - 超时封装:为任务设置上限时间,避免长时间挂起
function withTimeout(promise, ms) {
return new Promise((resolve, reject) => {
const id = setTimeout(() => reject(new Error('Timeout')), ms);
promise.then(v => { clearTimeout(id); resolve(v); },
e => { clearTimeout(id); reject(e); });
});
}
const controller = new AbortController();
fetch('/api', { signal: controller.signal }).catch(() => {});
controller.abort();
重试与指数退避
- 退避策略:每次失败后增加等待时间(如乘以系数和随机抖动)
- 最大次数:设定最大重试次数,避免无限重试
async function retry(fn, { retries = 3, base = 200 } = {}) {
for (let i = 0; i < retries; i++) {
try { return await fn(); } catch (e) {
const wait = base * Math.pow(2, i) + Math.random() * 100;
await new Promise(r => setTimeout(r, wait));
}
}
throw new Error('Retry failed');
}
Worker 与主线程分工
- Web Worker:将 CPU 密集计算下放到后台线程,避免阻塞 UI
- Comlink 等封装:简化主线程与 Worker 的消息通信
- 实践:将长循环、解析与压缩、图像处理等移入 Worker
任务调度与空闲时间
- requestIdleCallback:在浏览器空闲时执行非关键任务(需考虑兼容性)
- 优先级调度:将关键交互相关任务放在更靠前的时机执行
requestIdleCallback(() => {
// 执行统计与轻量缓存清理等非关键工作
});
流与异步迭代
- ReadableStream:逐步消费响应体,降低内存峰值
- for await...of:遍历异步可迭代对象,按到达顺序处理数据块
async function readStream(stream) {
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 处理 value
}
}
小结
- 充分理解宏/微任务队列与渲染时机
- 针对场景选择顺序化、并行与限流策略
- 为任务提供取消与超时,提升稳健性
- 使用 Worker 与流式处理降低卡顿与内存占用
Promise 组合与模式
- 并行组合:
Promise.all聚合多个独立任务 - 容错组合:
Promise.allSettled收集成功与失败结果 - 最快结果:
Promise.race获取最先完成者 - 顺序执行:通过
reduce链式串联
const tasks = [t1, t2, t3];
const results = await Promise.allSettled(tasks.map(t => t()));
const seq = tasks.reduce((p, t) => p.then(() => t()), Promise.resolve());
await seq;
异步迭代器管道
- 生成器组合:构造可读的异步数据处理流水线
- 背压协作:在拉取下一块数据前完成当前处理
async function* mapAsync(iterable, fn) {
for await (const x of iterable) yield fn(x);
}
async function* filterAsync(iterable, pred) {
for await (const x of iterable) if (pred(x)) yield x;
}
简单任务队列示例
class TaskQueue {
constructor(concurrency = 4) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
push(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.next();
});
}
next() {
if (this.running >= this.concurrency || this.queue.length === 0) return;
const { task, resolve, reject } = this.queue.shift();
this.running++;
Promise.resolve()
.then(task)
.then(resolve, reject)
.finally(() => { this.running--; this.next(); });
}
}
UI 与空闲调度策略
- 分片渲染:将大列表渲染拆分为多个宏任务,避免长任务阻塞
- 优先关键路径:输入响应与动画优先,其次网络与非关键计算
function chunkedRender(items, chunk = 100) {
let i = 0;
function run() {
const end = Math.min(i + chunk, items.length);
for (; i < end; i++) {
// 渲染 items[i]
}
if (i < items.length) setTimeout(run, 0);
}
run();
}
Node.js 阶段简述
- timers:处理
setTimeout/setInterval - pending callbacks:上一轮循环的 I/O 回调
- poll:检索新的 I/O 事件
- check:处理
setImmediate - close callbacks:关闭事件的回调
实战踩坑清单
- 长任务阻塞:在主线程执行密集计算导致掉帧 → 使用 Worker 或分片
- 遗漏微任务:链式
then未正确处理异常 → 在链尾使用catch - 无取消能力:长时间请求无法中断 → 引入
AbortController - 竞态条件:多个请求互相覆盖状态 → 使用令牌验证或版本戳
- 资源打爆:过度并发压垮服务 → 使用限流与退避策略
参考实践
- 在 UI 场景优先保证交互与动画时序
- 在数据拉取场景优先引入取消与超时
- 在高并发场景优先实现限流与背压
- 在长耗时任务优先下放到 Worker
Service Worker 与缓存策略(概览)
- Service Worker:拦截网络请求,实现离线与缓存
- Cache Storage:以键值存储响应体,用于静态资源加速
self.addEventListener('install', e => {
e.waitUntil(caches.open('v1').then(cache => cache.addAll(['/index.html','/app.js'])));
});
self.addEventListener('fetch', e => {
e.respondWith(caches.match(e.request).then(res => res || fetch(e.request)));
});
WebSocket 心跳与重连
- 心跳:定期发送 ping 保持连接
- 重连:在断线后指数退避重连,避免雪崩
function connect() {
const ws = new WebSocket('wss://example.com/socket');
let timer;
ws.onopen = () => { timer = setInterval(() => ws.send('ping'), 30000); };
ws.onclose = () => { clearInterval(timer); setTimeout(connect, 2000); };
}
connect();
关键性能指标与监测
- FCP/LCP/CLS:首绘、最大内容绘制、累计布局偏移
- TTI/TBT:可交互时间与阻塞总时长
- Performance API:采样与标记
performance.mark('start');
// ... 任务
performance.mark('end');
performance.measure('task', 'start', 'end');
rAF vs setTimeout
- requestAnimationFrame:与浏览器刷新同步,适合动画
- setTimeout:时间驱动,可能与帧率不同步,导致抖动
function animate(el) {
let x = 0;
function step() {
x += 1;
el.style.transform = `translateX(${x}px)`;
requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
2025/2026 语言演进与异步
- Temporal(ES2026 预计)
- 高精度时间与时区处理,替代历史 Date 的缺陷
- 时间段/持续时间(Temporal.Duration)便于表达延迟与节奏
- 时区化时间(Temporal.ZonedDateTime)简化跨区调度
// 以本地时区安排下一次任务(示例)
const now = Temporal.Now.zonedDateTimeISO();
const next = now.add({ minutes: 5 });
const delay = next.epochMilliseconds - now.epochMilliseconds;
setTimeout(() => {
// 执行定时任务
}, delay);
- Iterator Helpers(ES2025)
- 惰性管道:不构建中间数组即可过滤/映射数据流
- 与异步场景协作:在主线程压力较小时批量推进迭代
const it = [1,2,3,4,5].values();
const out = it.filter(x => x % 2).map(x => x * 2).take(2);
for (const v of out) { /* 1->2, 3->6 */ }
- import defer(ES2026 预计)
- 延迟部分导入的初始化,优化首屏与关键路径
// 假设:对大型非关键模块进行延迟求值
import defer heavy from './heavy-module.js';
// 业务关键路径运行完毕后再触发
queueMicrotask(() => heavy.init());
实战建议
- 使用 Temporal 统一时间计算,避免 Date 的时区陷阱
- 在大数据迭代管道中采用 Iterator Helpers,降低内存峰值
- 配合 import defer 与空闲调度,将非关键初始化迁移到次时机