【前端面试系列】事件循环机制深度剖析

一、基础概念与历史背景

1.1 为什么需要事件循环?

JavaScript 最初被设计为浏览器脚本语言,其单线程特性源于其主要用途:处理用户交互和操作DOM。如果没有事件循环机制,将会面临以下问题:

  1. 阻塞问题

    • 同步操作会阻塞UI渲染
    • 长时间运算会导致页面无响应
    • 网络请求等IO操作会造成等待
  2. 资源利用

    • CPU在等待IO时完全空闲
    • 无法充分利用多核处理器
    • 内存使用效率低下
  3. 用户体验

    • 界面卡顿
    • 交互延迟
    • 动画不流畅

1.2 进程与线程

// 进程与线程的关系示例
浏览器进程 {
    GUI渲染线程
    JS引擎线程
    事件触发线程
    定时触发器线程
    异步HTTP请求线程
    ...
}
1.2.1 浏览器多进程架构
  • Browser进程:浏览器的主进程
  • Renderer进程:渲染进程,每个标签页独立
  • GPU进程:处理GPU任务
  • Network进程:处理网络请求
  • Plugin进程:插件进程
1.2.2 渲染进程的多线程
  1. GUI渲染线程

    • 负责渲染页面
    • 解析HTML、CSS
    • 构建DOM树和渲染树
  2. JS引擎线程

    • 解析执行JavaScript代码
    • V8引擎的主线程
    • 与GUI渲染线程互斥
  3. 事件触发线程

    • 管理事件队列
    • 协调事件循环
    • 分发事件到JS引擎线程
  4. 定时器触发线程

    • 管理setTimeout/setInterval
    • 计时完毕后将回调加入事件队列
  5. 异步HTTP请求线程

    • 处理XMLHttpRequest
    • 处理Fetch请求
    • 请求完成后将回调加入事件队列

1.3 JavaScript的运行时环境

// JavaScript运行时环境示意
JavaScript Runtime = {
    调用栈(Call Stack),
    堆内存(Heap),
    任务队列(Task Queue) {
        宏任务队列(Macrotask Queue),
        微任务队列(Microtask Queue)
    },
    Web APIs {
        DOM,
        AJAX,
        setTimeout,
        ...
    }
}
1.3.1 内存模型
// 内存分配示例
// 栈内存
let a = 1;
let b = 'string';
let c = true;

// 堆内存
let obj = {
    name: 'object',
    data: [1, 2, 3]
};

let arr = new Array(1000);
1.3.2 执行上下文
// 执行上下文示例
// 全局执行上下文
var global = 'global';

function outer() {
    // outer函数执行上下文
    var a = 1;
    
    function inner() {
        // inner函数执行上下文
        var b = 2;
        console.log(a, b);
    }
    
    inner();
}

outer();

二、事件循环的核心机制

2.1 调用栈详解

// 调用栈运行机制示例
function multiply(a, b) {
    return a * b;
}

function square(n) {
    return multiply(n, n);
}

function printSquare(n) {
    var squared = square(n);
    console.log(squared);
}

printSquare(4);

// 调用栈变化过程
// 1. [printSquare]
// 2. [printSquare, square]
// 3. [printSquare, square, multiply]
// 4. [printSquare, square]
// 5. [printSquare]
// 6. []
2.1.1 调用栈溢出
// 栈溢出示例
function recursion() {
    recursion();
}

// 防止栈溢出的解决方案
function safeRecursion(n) {
    if (n === 0) return;
    
    // 使用setTimeout将递归放入宏任务队列
    setTimeout(() => {
        console.log(n);
        safeRecursion(n - 1);
    }, 0);
}

safeRecursion(10000); // 不会栈溢出

2.2 任务队列详解

2.2.1 宏任务(Macrotask)完整列表
// 宏任务类型及其优先级
宏任务优先级 = {
    1: 'script(整体代码)',
    2: 'setTimeout/setInterval',
    3: 'setImmediate(Node环境)',
    4: 'requestAnimationFrame(浏览器环境)',
    5: 'I/O操作',
    6: 'UI rendering',
    7: 'MessageChannel',
    8: 'setImmediate(IE专用)'
}
2.2.2 微任务(Microtask)完整列表
// 微任务类型及其优先级
微任务优先级 = {
    1: 'process.nextTick(Node环境)',
    2: 'Promise.then/catch/finally',
    3: 'queueMicrotask',
    4: 'MutationObserver',
    5: 'IntersectionObserver',
    6: 'ResizeObserver'
}

三、事件循环的详细执行流程

3.1 完整的事件循环流程图

// 事件循环伪代码表示
while (true) {
    // 1. 执行同步代码,直到调用栈清空
    executeSynchronousCode();
    
    // 2. 执行微任务队列的所有任务
    while (microTaskQueue.length > 0) {
        executeMicroTask(microTaskQueue.dequeue());
    }
    
    // 3. 执行一个宏任务
    if (macroTaskQueue.length > 0) {
        executeMacroTask(macroTaskQueue.dequeue());
    }
    
    // 4. 如果需要,进行UI渲染
    if (shouldRender) {
        render();
    }
}

3.2 实际案例分析

3.2.1 基础案例
console.log('1'); // 同步代码

setTimeout(() => {
    console.log('2'); // 宏任务
}, 0);

Promise.resolve().then(() => {
    console.log('3'); // 微任务
});

console.log('4'); // 同步代码

// 输出顺序:1 -> 4 -> 3 -> 2
3.2.2 复杂案例
console.log('1'); // 同步

setTimeout(() => {
    console.log('2'); // 宏任务1
    Promise.resolve().then(() => {
        console.log('3'); // 宏任务1中的微任务
    });
}, 0);

new Promise((resolve) => {
    console.log('4'); // 同步
    resolve();
}).then(() => {
    console.log('5'); // 微任务1
    setTimeout(() => {
        console.log('6'); // 微任务1中的宏任务
    }, 0);
});

console.log('7'); // 同步

// 输出顺序:1 -> 4 -> 7 -> 5 -> 2 -> 3 -> 6

3.3 特殊场景分析

3.3.1 嵌套的微任务
Promise.resolve().then(() => {
    console.log('1'); // 第一个微任务
    
    Promise.resolve().then(() => {
        console.log('2'); // 嵌套的微任务
    });
}).then(() => {
    console.log('3'); // 第二个微任务
});

// 输出顺序:1 -> 2 -> 3
3.3.2 微任务与DOM更新
// DOM更新与微任务的关系
const div = document.createElement('div');
div.style.width = '100px';
document.body.appendChild(div);

Promise.resolve().then(() => {
    console.log(div.offsetWidth); // 100
    div.style.width = '200px';
    console.log(div.offsetWidth); // 100
    
    setTimeout(() => {
        console.log(div.offsetWidth); // 200
    }, 0);
});

四、Promise与异步编程

4.1 Promise的实现原理

// 简化版Promise实现
class MyPromise {
    constructor(executor) {
        this.status = 'pending';
        this.value = undefined;
        this.reason = undefined;
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];
        
        const resolve = (value) => {
            if (this.status === 'pending') {
                this.status = 'fulfilled';
                this.value = value;
                this.onFulfilledCallbacks.forEach(fn => fn());
            }
        };
        
        const reject = (reason) => {
            if (this.status === 'pending') {
                this.status = 'rejected';
                this.reason = reason;
                this.onRejectedCallbacks.forEach(fn => fn());
            }
        };
        
        try {
            executor(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }
    
    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
        
        const promise2 = new MyPromise((resolve, reject) => {
            if (this.status === 'fulfilled') {
                queueMicrotask(() => {
                    try {
                        const x = onFulfilled(this.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            }
            
            if (this.status === 'rejected') {
                queueMicrotask(() => {
                    try {
                        const x = onRejected(this.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            }
            
            if (this.status === 'pending') {
                this.onFulfilledCallbacks.push(() => {
                    queueMicrotask(() => {
                        try {
                            const x = onFulfilled(this.value);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
                
                this.onRejectedCallbacks.push(() => {
                    queueMicrotask(() => {
                        try {
                            const x = onRejected(this.reason);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
            }
        });
        
        return promise2;
    }
}

4.2 Promise的常见应用模式

4.2.1 Promise.all 实现
Promise.myAll = function(promises) {
    return new Promise((resolve, reject) => {
        let results = [];
        let completed = 0;
        
        if (promises.length === 0) {
            resolve(results);
            return;
        }
        
        promises.forEach((promise, index) => {
            Promise.resolve(promise).then(
                value => {
                    results[index] = value;
                    completed++;
                    
                    if (completed === promises.length) {
                        resolve(results);
                    }
                },
                reason => {
                    reject(reason);
                }
            );
        });
    });
};
4.2.2 Promise.race 实现
Promise.myRace = function(promises) {
    return new Promise((resolve, reject) => {
        promises.forEach(promise => {
            Promise.resolve(promise).then(resolve, reject);
        });
    });
};

五、Node.js事件循环详解

5.1 Node.js事件循环的六个阶段

// Node.js事件循环的阶段顺序
const phases = {
    1: 'timers', // setTimeout, setInterval
    2: 'pending callbacks', // 执行延迟到下一个循环迭代的 I/O 回调
    3: 'idle, prepare', // 仅系统内部使用
    4: 'poll', // 检索新的 I/O 事件,执行 I/O 相关的回调
    5: 'check', // setImmediate() 回调
    6: 'close callbacks' // 关闭的回调函数
};

5.2 Node.js与浏览器的事件循环区别

5.2.1 微任务执行时机
// 浏览器环境
setTimeout(() => {
    console.log('timeout1');
    Promise.resolve().then(() => console.log('promise1'));
}, 0);

setTimeout(() => {
    console.log('timeout2');
    Promise.resolve().then(() => console.log('promise2'));
}, 0);

// 浏览器输出:timeout1 -> promise1 -> timeout2 -> promise2

// Node.js环境(v11之前)
setTimeout(() => {
    console.log('timeout1');
    Promise.resolve().then(() => console.log('promise1'));
}, 0);

setTimeout(() => {
    console.log('timeout2');
    Promise.resolve().then(() => console.log('promise2'));
}, 0);

// Node.js输出(v11之前):timeout1 -> timeout2 -> promise1 -> promise2
5.2.2 process.nextTick
// process.nextTick 优先级演示
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));

// 输出顺序:
// 1. nextTick
// 2. promise
// 3. timeout
// 4. immediate

5.3 Node.js性能优化

// 优化示例:避免阻塞事件循环
// 不好的实践
function processLargeArray(array) {
    array.forEach(item => {
        // 耗时操作
        heavyComputation(item);
    });
}

// 优化后的实践
async function processLargeArray(array) {
    const chunkSize = 1000;
    for (let i = 0; i < array.length; i += chunkSize) {
        const chunk = array.slice(i, i + chunkSize);
        await new Promise(resolve => setImmediate(resolve));
        chunk.forEach(item => heavyComputation(item));
    }
}

六、面试题解析

6.1 经典面试题

6.1.1 输出顺序题
// 问题1:以下代码输出顺序是什么?
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

async function async2() {
    console.log('async2');
}

console.log('script start');

setTimeout(() => {
    console.log('setTimeout');
}, 0);

async1();

new Promise((resolve) => {
    console.log('promise1');
    resolve();
}).then(() => {
    console.log('promise2');
});

console.log('script end');

// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
6.1.2 Promise相关题
// 问题2:以下代码输出什么?
Promise.resolve().then(() => {
    console.log('then1');
    Promise.resolve().then(() => {
        console.log('then1-1');
    });
}).then(() => {
    console.log('then2');
});

Promise.resolve().then(() => {
    console.log('then3');
});

// 输出顺序:
// then1
// then3
// then1-1
// then2

6.2 进阶面试题

6.2.1 实现限制并发的Promise调度器
class Scheduler {
    constructor(limit) {
        this.queue = [];
        this.limit = limit;
        this.running = 0;
    }

    add(promiseCreator) {
        return new Promise((resolve) => {
            this.queue.push({
                promiseCreator,
                resolve
            });
            this.run();
        });
    }

    run() {
        if (this.running >= this.limit || this.queue.length === 0) {
            return;
        }

        this.running++;
        const { promiseCreator, resolve } = this.queue.shift();

        promiseCreator().then((result) => {
            resolve(result);
            this.running--;
            this.run();
        });
    }
}

// 使用示例
const scheduler = new Scheduler(2);
const addTask = (time, value) => {
    scheduler.add(() => 
        new Promise(resolve => {
            setTimeout(() => {
                console.log(value);
                resolve();
            }, time);
        })
    );
};

addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 输出: 2 -> 1 -> 3 -> 4
6.2.2 手写防抖节流
// 防抖
function debounce(fn, delay) {
    let timer = null;
    return function(...args) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}

// 节流
function throttle(fn, delay) {
    let last = 0;
    return function(...args) {
        const now = Date.now();
        if (now - last > delay) {
            last = now;
            fn.apply(this, args);
        }
    };
}

// 使用示例
const debouncedScroll = debounce(() => {
    console.log('scroll');
}, 200);

const throttledScroll = throttle(() => {
    console.log('scroll');
}, 200);

window.addEventListener('scroll', debouncedScroll);
window.addEventListener('scroll', throttledScroll);

6.3 实战面试题

6.3.1 实现一个带并发限制的异步调度器
class AsyncPool {
    constructor(limit) {
        this.limit = limit;
        this.running = 0;
        this.queue = [];
    }

    async add(fn) {
        if (this.running >= this.limit) {
            await new Promise(resolve => this.queue.push(resolve));
        }
        
        this.running++;
        try {
            return await fn();
        } finally {
            this.running--;
            if (this.queue.length > 0) {
                this.queue.shift()();
            }
        }
    }
}

// 使用示例
const pool = new AsyncPool(2);
const sleep = (time) => new Promise(resolve => setTimeout(resolve, time));

async function test() {
    console.time('total');
    const tasks = [
        () => sleep(1000).then(() => console.log(1)),
        () => sleep(500).then(() => console.log(2)),
        () => sleep(300).then(() => console.log(3)),
        () => sleep(400).then(() => console.log(4))
    ];

    await Promise.all(tasks.map(task => pool.add(task)));
    console.timeEnd('total');
}

test();

七、性能优化建议

7.1 代码层面优化

  1. 合理使用微任务和宏任务
// 不好的实践
function processData(items) {
    items.forEach(item => {
        setTimeout(() => {
            // 处理逻辑
        }, 0);
    });
}

// 好的实践
async function processData(items) {
    const chunks = chunk(items, 1000);
    for (const chunk of chunks) {
        await new Promise(resolve => setTimeout(resolve, 0));
        chunk.forEach(item => {
            // 处理逻辑
        });
    }
}
  1. 避免长时间占用事件循环
// 不好的实践
function heavyComputation() {
    for (let i = 0; i < 1000000; i++) {
        // 复杂计算
    }
}

// 好的实践
async function heavyComputation() {
    const total = 1000000;
    const batchSize = 1000;
    
    for (let i = 0; i < total; i += batchSize) {
        await new Promise(resolve => setTimeout(resolve, 0));
        for (let j = 0; j < batchSize; j++) {
            // 复杂计算
        }
    }
}

7.2 工程实践建议

  1. 使用Performance API监控性能
  2. 合理划分任务优先级
  3. 利用Web Workers处理密集计算
  4. 采用虚拟列表优化大数据渲染
  5. 使用RequestAnimationFrame优化动画

八、总结

事件循环是JavaScript实现异步编程的核心机制,深入理解它对于:

  • 编写高性能代码
  • 解决复杂异步问题
  • 优化用户体验
    都有重要意义。

建议读者:

  1. 多写demo验证概念
  2. 关注各种框架的异步实现
  3. 持续学习新的异步编程模式

写在最后

感谢您花时间阅读这篇文章! 🙏

如果您觉得文章对您有帮助,欢迎:

  • 关注我的技术博客:徐白知识星球 📚
  • 关注微信公众号:徐白知识星球

我会持续输出高质量的前端技术文章,分享实战经验和技术心得。

共同成长

  • 欢迎在评论区留言交流
  • 有问题随时后台私信
  • 定期举办技术分享会
  • 更多精彩内容持续更新中…

让我们一起在技术的道路上携手前行! 💪

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

徐白1177

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值