事件循环与任务队列
题目1: 解释事件循环机制,什么是微任务和宏任务?
答案: 事件循环是JavaScript的一种机制,用于执行代码、收集和处理事件以及执行排队的子任务。
微任务和宏任务是两种不同类型的任务:
- 微任务(Microtask):优先级较高,在当前任务执行结束后立即执行。例如Promise的回调、MutationObserver等。
- 宏任务(Macrotask):优先级较低,在下一次事件循环中执行。例如setTimeout、setInterval、I/O操作等。
事件循环的基本过程:
- 执行同步代码,这属于宏任务
- 执行完所有同步代码后,执行栈为空,查询是否有微任务需要执行
- 执行所有微任务
- 执行完所有微任务后,如有必要会渲染页面
- 开始下一轮事件循环,执行宏任务中的异步代码
扩展:
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
// 输出顺序:1, 4, 3, 2
题目2: setTimeout
和 Promise
的执行顺序如何?
答案: Promise
的回调(.then()
、.catch()
)属于微任务,而 setTimeout
的回调属于宏任务。在事件循环中,微任务总是在下一个宏任务之前执行。
因此,即使 setTimeout
的延迟时间设置为0,Promise
的回调也会先于 setTimeout
的回调执行。
扩展:
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
// 输出顺序:sync, promise, timeout
题目3: 什么是 requestAnimationFrame
,如何优化动画?
答案: requestAnimationFrame
是浏览器用于定时循环操作的一个接口,主要用于动画,使用这个API,可以在浏览器下一次重绘之前执行指定的回调函数。
优化动画的方法:
- 使用
requestAnimationFrame
代替setTimeout
或setInterval
- 避免频繁的DOM操作,可以使用虚拟DOM
- 使用 CSS3 动画代替 JavaScript 动画
- 使用
transform
和opacity
属性进行动画,因为这些属性可以由 GPU 加速
扩展:
function animate() {
// 更新动画
updateAnimation();
// 继续下一帧
requestAnimationFrame(animate);
}
// 开始动画
requestAnimationFrame(animate);
题目4: 如何理解JavaScript的单线程特性?
答案: JavaScript是单线程的,这意味着它只有一个调用栈,同一时间只能执行一个任务。这个特性是为了简化编程模型,避免多线程编程中的复杂性。
单线程的影响:
- 长时间运行的脚本会阻塞页面响应
- 需要使用异步编程来处理耗时操作
- 事件循环和任务队列的概念由此产生
扩展: 虽然JavaScript是单线程的,但浏览器不是。浏览器可以使用Web Workers在后台线程中运行脚本,不会影响页面的性能。
题目5: 什么是任务队列(Task Queue)?它在事件循环中的作用是什么?
答案: 任务队列是一个先进先出的队列,用于存储待执行的任务(主要是回调函数)。在事件循环中,当主线程空闲时,会从任务队列中取出任务执行。
任务队列的作用:
- 存储异步操作的回调函数
- 确保任务按照正确的顺序执行
- 防止长时间运行的任务阻塞主线程
扩展: 实际上,浏览器维护多个任务队列,不同类型的任务可能在不同的队列中。例如,鼠标点击事件和 setTimeout 回调可能在不同的队列中。
题目6: 解释一下 JavaScript 中的 "堆" 和 "栈"。
答案:
- 堆(Heap):用于存储对象、数组等复杂数据类型。
- 栈(Stack):用于存储基本数据类型和对象的引用。
特点:
- 栈的分配速度比堆快,但容量有限。
- 堆的分配速度较慢,但容量大。
扩展: 当我们创建一个对象时,对象本身存储在堆中,而对象的引用存储在栈中。这就是为什么对象是按引用传递的原因。
题目7: 什么是 Web Workers?它们如何影响事件循环?
答案: Web Workers 是一种可以在后台线程中运行脚本的技术,不会影响页面的性能。
Web Workers 的特点:
- 可以执行长时间运行的脚本而不阻塞主线程
- 不能直接访问 DOM
- 通过消息传递与主线程通信
Web Workers 对事件循环的影响:
- Web Workers 有自己的事件循环,不会直接影响主线程的事件循环
- 主线程可以通过 postMessage 向 Worker 发送消息,这个操作是异步的
扩展:
// 创建一个新的 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 是单线程的,通常不会出现经典的死锁情况,但在某些情况下,可能会出现类似死锁的情况,我们称之为 "事件循环阻塞" 或 "无限循环"。
可能导致事件循环阻塞的情况:
- 无限循环
- 递归调用没有正确的终止条件
- 同步 AJAX 请求
扩展: 为了避免事件循环阻塞,应该:
- 使用异步操作处理耗时任务
- 将大任务分割成小任务,使用 setTimeout 等方法让出控制权
- 使用 Web Workers 处理复杂计算
题目9: 什么是 "任务分割"(Task splitting)?为什么它在 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)及其与事件循环的关系。
答案: 事件委托是一种事件处理模式,它利用事件冒泡,允许我们在父元素上添加一个事件监听器,来处理子元素上的事件。
事件委托的优点:
- 减少事件监听器的数量,提高性能
- 动态添加的元素也能响应事件
- 减少内存使用
事件委托与事件循环的关系:
- 事件委托可以减少事件监听器的数量,从而减少事件循环中需要处理的任务数量
- 当事件触发时,事件处理函数会被添加到任务队列中,等待事件循环处理
扩展:
document.getElementById('parent-list').addEventListener('click', function(e) {
if(e.target && e.target.nodeName == "LI") {
console.log("List item ", e.target.id, " was clicked!");
}
});
这个例子展示了如何使用事件委托来处理列表项的点击事件,而不需要为每个列表项单独添加事件监听器。