javascript 的事件循环机制(Event Loop)是怎么样的?
javascript是单线程的,所以是通过事件循环机制在 JS 运行时处理异步操作的,在浏览器环境和nodejs环境它们的实现细节和优先级处理有所不同,我们先看浏览器环境下的事件循环机制,整体来说浏览器下的事件循环机制相对简单,他由四部分组成:
调用栈(Call Stack)
调用栈是一个 LIFO(Last In, First Out)后进先出的结构,用于存储在代码执行过程中创建的函数调用。每当一个函数被调用时,它会被添加到调用栈顶部。当函数执行完成后,它会从调用栈顶部弹出。
1 | function first() { |
- second() 被调用并推入调用栈。
- second() 调用了 first(),first() 被推入调用栈。
- first() 完成后,从调用栈弹出。
- second() 完成后,从调用栈弹出。
消息队列(Task Queue)
消息队列是一个 FIFO(先进先出)结构,用于存储待处理的异步任务(如事件处理、回调函数)。常见的任务包括:
用户交互事件(点击、键盘输入等)的回调函数。
setTimeout、setInterval 的回调函数。
网络请求(如 fetch、XHR)的回调
虽然 fetch 返回一个 Promise,但实际的网络请求是通过浏览器内部机制异步处理的。这意味着网络请求的启动和响应的处理都是在宏任务的上下文中完成的
requestAnimationFrame
DOM 事件
注意:消息队列和宏任务(Macro Task)的区别
- 包含关系:消息队列是存储宏任务的容器,宏任务是消息队列中的一个个任务。可以说消息队列由一系列宏任务组成。
- 执行顺序:事件循环机制中,主线程执行完当前的同步代码后,会先检查并执行所有的微任务队列(Microtask Queue)。只有在微任务队列为空的情况下,事件循环才会从消息队列中取出下一个宏任务并执行。
- 处理机制:
- 宏任务:每次事件循环只执行一个宏任务。执行完宏任务后,立即处理微任务队列中的所有微任务,然后再从消息队列中取出下一个宏任务。
- 消息队列:消息队列中可以包含多个宏任务,但每次事件循环只处理一个宏任务。只有在当前宏任务完成且微任务执行完毕后,才会取下一个宏任务。
- 处理机制:
- 优先级:微任务的优先级高于宏任务。每次宏任务执行完后,事件循环会先检查并执行所有微任务,然后再继续下一个宏任务。消息队列本身没有优先级的概念,而是所有存储的任务都是按先进先出(FIFO)顺序执行。
微任务(Microtask Queue)
微任务队列用于处理高优先级的异步任务。微任务通常包括:
Promise 的回调(如 .then()、.catch())。
MutationObserver 的回调
MutationObserver 是一种用于监听和响应 DOM 树中更改的 Web API。当 DOM 元素的结构、属性或文本内容发生变化时,MutationObserver 可以检测到这些变化,并执行相应的回调函数
queueMicrotask() 明确调度的微任务。
事件循环(Event Loop)
事件循环是一个持续运行的过程,其主要职责是查看执行栈是否为空,如果为空则检查消息队列中是否有任务待处理。如果有,则将队列中的第一个任务移入执行栈并开始执行。事件循环的每一次循环迭代称为“tick”。
事件循环的工作步骤
- 执行栈处理同步代码:当 JavaScript 引擎开始运行时,所有的同步任务(函数调用)都被推入执行栈中并立即执行。执行栈会持续运行直到栈为空。
- 执行微任务:执行栈清空后,事件循环检查微任务队列。它会一次性执行完所有在微任务队列中的微任务,直到队列为空。
- 执行宏任务:在微任务执行完毕后,事件循环会从消息队列中取出第一个任务(宏任务),将其推入执行栈并执行。
- 渲染更新:在每个宏任务之间,浏览器有机会更新渲染。如果 DOM 发生了更改(例如插入元素、修改样式等),浏览器会重新渲染 UI。
- 重复步骤 1-4:事件循环不断重复这些步骤,处理同步任务、微任务、宏任务、并更新渲染。
1 | console.log('Start'); // 同步任务 |
执行顺序如下:
1. 同步代码先执行:console.log(‘Start’) 和 console.log(‘End’) 是同步任务,依次推入执行栈并执行,输出:
1 | Start |
- 微任务执行:Promise.resolve().then(…) 是微任务,它的回调函数被推入微任务队列。由于微任务优先于宏任务执行,所以 console.log(‘Promise’) 先输出:
1
Promise
- 宏任务执行:setTimeout 的回调函数被推入消息队列,在所有微任务完成后执行。最终输出:
1
Timeout
最终输出顺序:
1 | Start |
总结
浏览器中的事件循环机制是管理 JavaScript 异步行为的核心,通过执行栈、消息队列、微任务队列的协同工作,实现了非阻塞、响应迅速的编程模型。了解事件循环有助于优化代码性能,避免阻塞主线程,提高用户体验。