Skip to content

事件循环与任务队列

题目1: 解释事件循环机制,什么是微任务和宏任务?

答案: 事件循环是JavaScript的一种机制,用于执行代码、收集和处理事件以及执行排队的子任务。

微任务和宏任务是两种不同类型的任务:

  • 微任务(Microtask):优先级较高,在当前任务执行结束后立即执行。例如Promise的回调、MutationObserver等。
  • 宏任务(Macrotask):优先级较低,在下一次事件循环中执行。例如setTimeout、setInterval、I/O操作等。

事件循环的基本过程:

  1. 执行同步代码,这属于宏任务
  2. 执行完所有同步代码后,执行栈为空,查询是否有微任务需要执行
  3. 执行所有微任务
  4. 执行完所有微任务后,如有必要会渲染页面
  5. 开始下一轮事件循环,执行宏任务中的异步代码

扩展:

javascript
console.log('1');
setTimeout(() => {
    console.log('2');
}, 0);
Promise.resolve().then(() => {
    console.log('3');
});
console.log('4');

// 输出顺序:1, 4, 3, 2

题目2: setTimeoutPromise 的执行顺序如何?

答案: Promise 的回调(.then().catch())属于微任务,而 setTimeout 的回调属于宏任务。在事件循环中,微任务总是在下一个宏任务之前执行。

因此,即使 setTimeout 的延迟时间设置为0,Promise 的回调也会先于 setTimeout 的回调执行。

扩展:

javascript
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');

// 输出顺序:sync, promise, timeout

题目3: 什么是 requestAnimationFrame,如何优化动画?

答案: requestAnimationFrame 是浏览器用于定时循环操作的一个接口,主要用于动画,使用这个API,可以在浏览器下一次重绘之前执行指定的回调函数。

优化动画的方法:

  1. 使用 requestAnimationFrame 代替 setTimeoutsetInterval
  2. 避免频繁的DOM操作,可以使用虚拟DOM
  3. 使用 CSS3 动画代替 JavaScript 动画
  4. 使用 transformopacity 属性进行动画,因为这些属性可以由 GPU 加速

扩展:

javascript
function animate() {
    // 更新动画
    updateAnimation();
    
    // 继续下一帧
    requestAnimationFrame(animate);
}

// 开始动画
requestAnimationFrame(animate);

题目4: 如何理解JavaScript的单线程特性?

答案: JavaScript是单线程的,这意味着它只有一个调用栈,同一时间只能执行一个任务。这个特性是为了简化编程模型,避免多线程编程中的复杂性。

单线程的影响:

  1. 长时间运行的脚本会阻塞页面响应
  2. 需要使用异步编程来处理耗时操作
  3. 事件循环和任务队列的概念由此产生

扩展: 虽然JavaScript是单线程的,但浏览器不是。浏览器可以使用Web Workers在后台线程中运行脚本,不会影响页面的性能。

题目5: 什么是任务队列(Task Queue)?它在事件循环中的作用是什么?

答案: 任务队列是一个先进先出的队列,用于存储待执行的任务(主要是回调函数)。在事件循环中,当主线程空闲时,会从任务队列中取出任务执行。

任务队列的作用:

  1. 存储异步操作的回调函数
  2. 确保任务按照正确的顺序执行
  3. 防止长时间运行的任务阻塞主线程

扩展: 实际上,浏览器维护多个任务队列,不同类型的任务可能在不同的队列中。例如,鼠标点击事件和 setTimeout 回调可能在不同的队列中。

题目6: 解释一下 JavaScript 中的 "堆" 和 "栈"。

答案:

  • 堆(Heap):用于存储对象、数组等复杂数据类型。
  • 栈(Stack):用于存储基本数据类型和对象的引用。

特点:

  • 栈的分配速度比堆快,但容量有限。
  • 堆的分配速度较慢,但容量大。

扩展: 当我们创建一个对象时,对象本身存储在堆中,而对象的引用存储在栈中。这就是为什么对象是按引用传递的原因。

题目7: 什么是 Web Workers?它们如何影响事件循环?

答案: Web Workers 是一种可以在后台线程中运行脚本的技术,不会影响页面的性能。

Web Workers 的特点:

  1. 可以执行长时间运行的脚本而不阻塞主线程
  2. 不能直接访问 DOM
  3. 通过消息传递与主线程通信

Web Workers 对事件循环的影响:

  • Web Workers 有自己的事件循环,不会直接影响主线程的事件循环
  • 主线程可以通过 postMessage 向 Worker 发送消息,这个操作是异步的

扩展:

javascript
// 创建一个新的 Worker
const worker = new Worker('worker.js');

// 向 Worker 发送消息
worker.postMessage('Hello, Worker!');

// 接收 Worker 的消息
worker.onmessage = function(event) {
    console.log('Received from worker:', event.data);
};

题目8: 解释一下 JavaScript 中的 "死锁" 概念。

答案: 虽然 JavaScript 是单线程的,通常不会出现经典的死锁情况,但在某些情况下,可能会出现类似死锁的情况,我们称之为 "事件循环阻塞" 或 "无限循环"。

可能导致事件循环阻塞的情况:

  1. 无限循环
  2. 递归调用没有正确的终止条件
  3. 同步 AJAX 请求

扩展: 为了避免事件循环阻塞,应该:

  1. 使用异步操作处理耗时任务
  2. 将大任务分割成小任务,使用 setTimeout 等方法让出控制权
  3. 使用 Web Workers 处理复杂计算

题目9: 什么是 "任务分割"(Task splitting)?为什么它在 JavaScript 中很重要?

答案: 任务分割是将一个大任务分解成多个小任务的技术,每个小任务在事件循环的不同轮次中执行。

任务分割的重要性:

  1. 防止长时间运行的脚本阻塞主线程
  2. 提高应用的响应性
  3. 允许浏览器在任务之间执行其他操作,如渲染更新

扩展:

javascript
function processArray(array) {
    const chunk = 1000;
    let index = 0;

    function doChunk() {
        let count = chunk;
        while (count-- && index < array.length) {
            // 处理 array[index]
            index++;
        }
        if (index < array.length) {
            // 还有更多要处理,安排下一个块
            setTimeout(doChunk, 0);
        }
    }

    doChunk();
}

题目10: 解释一下 "事件委托"(Event Delegation)及其与事件循环的关系。

答案: 事件委托是一种事件处理模式,它利用事件冒泡,允许我们在父元素上添加一个事件监听器,来处理子元素上的事件。

事件委托的优点:

  1. 减少事件监听器的数量,提高性能
  2. 动态添加的元素也能响应事件
  3. 减少内存使用

事件委托与事件循环的关系:

  • 事件委托可以减少事件监听器的数量,从而减少事件循环中需要处理的任务数量
  • 当事件触发时,事件处理函数会被添加到任务队列中,等待事件循环处理

扩展:

javascript
document.getElementById('parent-list').addEventListener('click', function(e) {
    if(e.target && e.target.nodeName == "LI") {
        console.log("List item ", e.target.id, " was clicked!");
    }
});

这个例子展示了如何使用事件委托来处理列表项的点击事件,而不需要为每个列表项单独添加事件监听器。