每天一个高级前端知识 - Day 2
今日主题:事件循环底层揭秘 - 微任务、宏任务与浏览器调度黑盒
核心概念:事件循环并非简单的"先进先出"
// 你以为的顺序?
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 实际输出:1, 4, 3, 2
🔬 事件循环的完整流程
┌───────────────────────────┐
│ 执行一个宏任务(MacroTask) │ ← script整体、setTimeout、setInterval、I/O
└───────────────────────────┘
↓
┌───────────────────────────┐
│ 清空微任务队列 │ ← Promise.then、MutationObserver、queueMicrotask
│ (所有微任务,直到为空) │ (包括新产生的微任务!)
└───────────────────────────┘
↓
┌───────────────────────────┐
│ 是否到达渲染时机? │ ← 约16.6ms一次
│ 执行requestAnimationFrame│
│ 执行重排/重绘/合成 │
└───────────────────────────┘
↓
┌───────────────────────────┐
│ 处理宏任务队列的下一个 │
└───────────────────────────┘
💣 经典陷阱与高级应用
陷阱1:微任务中的死循环
// 💀 这会卡死页面!微任务永不结束
function infiniteMicrotask() {
Promise.resolve().then(() => infiniteMicrotask());
}
// 宏任务永远得不到执行,页面无响应
陷阱2:setTimeout(fn, 0) 不是真正的0ms
// 实际延迟至少4ms(浏览器规范)
// 嵌套层级≥5时会强制最少4ms
let start = performance.now();
setTimeout(() => {
console.log(performance.now() - start); // 通常是4-5ms
}, 0);
🚀 高级实战:零延迟调度器
实现一个不会阻塞渲染的任务调度器,利用微任务执行时机突破性能瓶颈:
class Scheduler {
constructor() {
this.queue = [];
this.isProcessing = false;
}
// 添加任务(高优先级 - 微任务)
addMicroTask(fn) {
if (this.isProcessing) {
// 如果在渲染周期内,推迟到下一个微任务批次
queueMicrotask(() => fn());
} else {
Promise.resolve().then(fn);
}
}
// 添加任务(低优先级 - 空闲时执行)
addIdleTask(fn) {
if ('requestIdleCallback' in window) {
requestIdleCallback(fn, { timeout: 50 });
} else {
setTimeout(fn, 1);
}
}
// 批量处理大数据,避免卡顿
async processLargeArray(items, chunkSize = 100) {
let index = 0;
const processChunk = () => {
return new Promise((resolve) => {
const end = Math.min(index + chunkSize, items.length);
// 同步处理当前chunk
for (let i = index; i < end; i++) {
this.processItem(items[i]);
}
index = end;
if (index < items.length) {
// 让出主线程,给渲染和交互机会
setTimeout(() => resolve(processChunk()), 0);
} else {
resolve();
}
});
};
await processChunk();
}
processItem(item) {
// 实际处理逻辑
console.log('Processing:', item);
}
}
// 使用示例 - 处理10万条数据不卡顿
const scheduler = new Scheduler();
scheduler.processLargeArray([...Array(100000).keys()], 200);
🎯 今日挑战
实现一个带有优先级的任务队列,要求:
- 支持3个优先级:高(微任务)、中(下一宏任务)、低(requestIdleCallback)
- 高优先级任务可以"插队"
- 限制单次执行时间,避免长任务( > 50ms)
- 提供API手动让出主线程
参考实现(核心部分)
class PriorityTaskQueue {
constructor() {
this.high = []; // 微任务队列
mid: [], // 下一tick宏任务
low: [], // 空闲时执行
this.isPerformingMicrotask = false;
// 启动调度循环
this.schedule();
}
addTask(priority, fn) {
switch(priority) {
case 'high':
if (this.isPerformingMicrotask) {
// 当前正在执行微任务队列,放入队列避免递归深度爆炸
this.high.push(fn);
} else {
queueMicrotask(() => this.executeWithTimeCheck(fn));
}
break;
case 'medium':
setTimeout(() => this.executeWithTimeCheck(fn), 0);
break;
case 'low':
requestIdleCallback((deadline) => {
if (deadline.timeRemaining() > 0) {
this.executeWithTimeCheck(fn);
} else {
this.addTask('low', fn); // 重新调度
}
});
break;
}
}
executeWithTimeCheck(fn, maxTime = 50) {
const start = performance.now();
fn();
const duration = performance.now() - start;
if (duration > maxTime) {
console.warn(`长任务警告: ${duration.toFixed(2)}ms`);
}
}
schedule() {
// 持续处理高优先级队列
const processHighQueue = () => {
this.isPerformingMicrotask = true;
while (this.high.length) {
const fn = this.high.shift();
this.executeWithTimeCheck(fn);
}
this.isPerformingMicrotask = false;
queueMicrotask(processHighQueue);
};
queueMicrotask(processHighQueue);
}
}
📊 性能监控工具
// 监控长任务(Long Task API)
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn(`检测到长任务: ${entry.duration}ms`, entry);
// 上报到监控系统
}
});
observer.observe({ entryTypes: ['longtask'] });
明日预告:V8引擎的隐藏类与内联缓存 - 如何写出JIT友好的JavaScript代码
💡 延伸思考:你知道为什么for...in比for...of慢吗?明天揭晓答案!