Node.js Event Loop, Async Patterns & Streams
The event loop phases, timers and microtasks, promises and async/await, callback -> promise -> async evolution, backpressure with streams, and avoiding event loop blocking.
Event Loop Phases
Each loop iteration (tick) goes through phases: timers (setTimeout/setInterval) -> pending callbacks -> idle, prepare -> poll (IO) -> check (setImmediate) -> close callbacks. Microtasks (promise continuations, process.nextTick) run after every phase, before moving on.
console.log('1');
setTimeout(() => console.log('2'), 0); // timers
setImmediate(() => console.log('3')); // check
Promise.resolve().then(() => console.log('4'));// 微任务
process.nextTick(() => console.log('5')); // nextTick 优先级最高
// 输出:1 5 4 2 3(或 1 5 4 3 2,取决于 IO 阶段)
From Callbacks to async/await
// 回调地狱(旧写法)
getUser(1, (err, user) => {
getOrders(user.id, (err, orders) => {
// 越缩越深
});
});
// Promise
getUser(1)
.then(user => getOrders(user.id))
.then(orders => console.log(orders))
.catch(err => console.error(err));
// async/await:看起来像同步
async function main() {
try {
const user = await getUser(1);
const orders = await getOrders(user.id);
console.log(orders);
} catch (err) {
console.error(err);
}
}
main();
// 并行执行
const [a, b] = await Promise.all([fetchA(), fetchB()]);
Streams and Backpressure
Streams process data in chunks instead of buffering everything — a 10GB file can be hashed or uploaded with constant memory. Backpressure propagates automatically: a slow consumer pauses the producer.
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
// 旧写法:read.pipe(transform).pipe(write)
// 推荐写法:pipeline 自动处理错误与背压
await pipeline(
createReadStream('input.tar.gz'),
zlib.createGunzip(),
createWriteStream('output/')
);
// HTTP body 就是流
app.post('/upload', (req, res) => {
req.pipe(fs.createWriteStream(dest));
});
Keeping the Loop Unblocked
- Never run CPU-heavy sync code in request handlers — offload to worker_threads or a child process.
- Avoid the sync fs API (fs.readFileSync) in servers; it pauses all requests.
- JSON.parse of multi-MB payloads is expensive — validate payload size at the gateway.
- Track lag with perf_hooks or --inspect; the 'UV_THREADPOOL_SIZE' env increases the 4-thread pool for IO-heavy fs/crypto calls.
事件循环阶段
每轮循环(tick)依次经过:timers(setTimeout/setInterval)→ 待决回调 → idle/prepare → poll(IO)→ check(setImmediate)→ close 回调。微任务(Promise 续体、process.nextTick)在每个阶段结束后、进入下一阶段前执行。
console.log('1');
setTimeout(() => console.log('2'), 0); // timers
setImmediate(() => console.log('3')); // check
Promise.resolve().then(() => console.log('4'));// 微任务
process.nextTick(() => console.log('5')); // nextTick 优先级最高
// 输出:1 5 4 2 3(或 1 5 4 3 2,取决于 IO 阶段)
从回调到 async/await
// 回调地狱(旧写法)
getUser(1, (err, user) => {
getOrders(user.id, (err, orders) => {
// 越缩越深
});
});
// Promise
getUser(1)
.then(user => getOrders(user.id))
.then(orders => console.log(orders))
.catch(err => console.error(err));
// async/await:看起来像同步
async function main() {
try {
const user = await getUser(1);
const orders = await getOrders(user.id);
console.log(orders);
} catch (err) {
console.error(err);
}
}
main();
// 并行执行
const [a, b] = await Promise.all([fetchA(), fetchB()]);
流与背压
流按块处理数据而无需全部缓存——处理 10GB 文件内存占用恒定。背压会自动传递:消费方慢时生产方自动暂停。
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
// 旧写法:read.pipe(transform).pipe(write)
// 推荐写法:pipeline 自动处理错误与背压
await pipeline(
createReadStream('input.tar.gz'),
zlib.createGunzip(),
createWriteStream('output/')
);
// HTTP body 就是流
app.post('/upload', (req, res) => {
req.pipe(fs.createWriteStream(dest));
});
保持事件循环不阻塞
- 绝不在请求处理里做 CPU 密集同步计算——交给 worker_threads 或子进程。
- 服务端避免同步 fs API(fs.readFileSync),它会卡住所有请求。
- 解析数 MB 的 JSON 开销很大,在网关层就限制请求体大小。
- 用 perf_hooks 或 --inspect 监控事件循环延迟;fs/crypto 密集场景可调大 UV_THREADPOOL_SIZE(默认 4)。