事件循环(Event Loop)
事件循环是 JavaScript 运行时的核心调度机制,负责协调同步代码执行和异步任务处理。它的基本工作流程是:检查调用栈是否为空,如果为空则从任务队列中取出一个任务压入调用栈执行。事件循环在浏览器环境中由 JavaScript 引擎和浏览器渲染引擎协同实现。每一轮事件循环(tick)的处理顺序是:执行一个宏任务、清空所有微任务、执行渲染(如果浏览器判断需要重绘)。事件循环的不断重复使得 JavaScript 能够在单线程的限制下高效地处理异步操作,同时保持页面的响应性和流畅渲染。
调用栈(Call Stack)
调用栈是 JavaScript 引擎用于追踪函数调用关系的数据结构。每当一个函数被调用时,它的执行上下文(包括参数、局部变量和返回地址)会被压入调用栈。函数执行完毕后,它的执行上下文从调用栈中弹出。调用栈是后进先出(LIFO)的数据结构,最先进入的函数最后才能返回。JavaScript 是单线程的,在任何时刻只有一个执行上下文在调用栈的栈顶执行。如果调用栈中的函数执行时间过长(如复杂的同步计算),会阻塞事件循环,导致页面无法响应用户交互和渲染。这就是为什么耗时的计算应该使用异步方式或 Web Worker 来处理。
宏任务(Macrotask)
宏任务是事件循环中每次迭代处理的基本任务单元。宏任务队列中存放着所有待处理的宏任务,事件循环每次 tick 会从队列中取出一个宏任务执行。常见的宏任务来源包括:setTimeout 和 setInterval 的回调函数、I/O 事件回调、UI 渲染事件(如 click、scroll 等)、MessageChannel 和 postMessage 的回调、MutationObserver 的回调(在部分实现中)、requestAnimationFrame 的回调。宏任务的执行顺序遵循先进先出(FIFO)的原则,先注册的回调先执行。每个宏任务执行完毕后,事件循环会清空微任务队列,然后再执行下一个宏任务。
微任务(Microtask)
微任务是优先级高于宏任务的异步任务类型。微任务队列在每个宏任务执行完毕后被完全清空,这意味着微任务队列中所有的任务(包括在执行过程中新加入的微任务)都会在下一个宏任务开始之前被处理。常见的微任务来源包括:Promise 的 then/catch/finally 回调、MutationObserver 的回调、queueMicrotask 函数注册的任务。微任务的"全清空"特性使其具有比宏任务更高的优先级。在实际开发中,理解微任务的执行时机对于正确使用 Promise 和 async/await 至关重要。如果在微任务中产生了新的微任务,它会在同一轮 tick 中被处理,不会等到下一轮。
同步任务(Synchronous Task)
同步任务是指在 JavaScript 调用栈中立即执行的代码,包括变量声明、赋值、函数调用、条件判断、循环等基本操作。同步任务按照代码的书写顺序从上到下逐行执行,前一行代码执行完毕后才会执行下一行。同步任务是事件循环中最先处理的任务类型,在任何异步回调执行之前,当前的同步代码必须全部执行完毕。如果同步代码的执行时间过长,会阻塞事件循环,导致页面无法渲染和响应用户交互。因此,耗时的同步操作(如大量数据处理、复杂计算等)应该拆分为多个小任务,通过 setTimeout、requestAnimationFrame 或 Web Worker 等方式异步执行。
渲染任务(Rendering Task)
渲染任务是浏览器在事件循环中执行的页面更新操作。浏览器会以大约每秒 60 帧(即每 16.67ms 一帧)的频率尝试渲染页面。渲染操作在事件循环的特定时机执行:当一个宏任务执行完毕且微任务队列被清空后,浏览器会判断是否需要进行渲染。如果距离上次渲染的时间超过一帧的间隔,浏览器会执行渲染操作。渲染过程包括:计算样式(Recalculate Style)、布局(Layout/Paint)、绘制(Paint)和合成(Composite)。requestAnimationFrame 的回调在渲染之前执行,常用于在渲染前更新动画状态。理解渲染时机对于优化页面性能和避免掉帧非常重要。
Promise
Promise 是 JavaScript 中用于处理异步操作的对象,它代表一个异步操作的最终完成或失败的结果。Promise 有三种状态:pending(进行中)、fulfilled(已完成)和 rejected(已拒绝)。Promise 的 then/catch/finally 回调会被放入微任务队列中执行,这使得 Promise 的异步回调比 setTimeout 等宏任务回调具有更高的执行优先级。Promise 支持链式调用(then 方法返回新的 Promise),允许多个异步操作按顺序执行。Promise.all、Promise.race、Promise.allSettled 和 Promise.any 等静态方法提供了并行处理多个 Promise 的能力。async/await 语法是 Promise 的语法糖,使异步代码看起来更像同步代码。
async/await
async/await 是 ES2017 引入的语法特性,是基于 Promise 的语法糖。async 函数返回一个 Promise 对象,函数内部的 await 表达式会暂停函数执行并等待 Promise 的结果。await 表达式之后的代码会在 Promise 完成后作为微任务执行。async/await 使异步代码的书写和阅读更加直观,避免了 Promise 链式调用的复杂嵌套。在事件循环的视角下,async 函数中的同步部分会立即执行,而 await 之后的代码会被包装为微任务。理解 async/await 的事件循环行为对于编写正确的异步代码非常重要,特别是在涉及多个 await 表达式的复杂场景中。
setTimeout 与 setInterval
setTimeout 和 setInterval 是浏览器提供的定时器 API,用于延迟或周期性地执行代码。setTimeout 在指定的延迟时间后将回调函数放入宏任务队列中等待执行。setInterval 按照指定的间隔时间周期性地将回调放入宏任务队列。需要注意的是,延迟时间和间隔时间只是"最小等待时间",实际的执行时间可能更长,因为回调需要等待当前调用栈清空和前面的宏任务执行完毕后才能被执行。即使将 setTimeout 的延迟设为 0,回调也不会立即执行,而是在当前同步代码和微任务执行完毕后的下一轮事件循环中执行。clearTimeout 和 clearInterval 用于取消已注册的定时器。
queueMicrotask
queueMicrotask 是一个用于注册微任务的全局函数,它接受一个回调函数作为参数,将该回调作为微任务加入微任务队列。与 Promise.then 不同,queueMicrotask 不需要创建 Promise 对象,是一个更轻量的微任务注册方式。在需要执行一些不依赖 Promise 的异步操作时,queueMicrotask 比创建一个空 Promise 更高效。queueMicrotask 的行为与 Promise.then 的回调执行时机完全相同:在当前宏任务执行完毕后、下一个宏任务开始之前被处理。这个函数在需要精确控制微任务执行时机的场景中非常有用。
requestAnimationFrame
requestAnimationFrame 是浏览器提供的用于在下一次重绘之前执行动画更新的 API。它的回调函数会在浏览器执行渲染操作之前被调用,属于微任务清空之后、渲染之前的执行阶段。requestAnimationFrame 的主要优势是与浏览器的刷新频率同步,确保动画的流畅性和能效性。在事件循环中,requestAnimationFrame 的回调执行时机介于微任务和渲染任务之间。使用 requestAnimationFrame 实现的动画比使用 setTimeout/setInterval 实现的动画更流畅,因为它避免了不必要的重绘和掉帧。cancelAnimationFrame 用于取消已注册的动画帧回调。
UD5工具箱