面试回答: JavaScript 在渲染主线程上按调用栈同步执行。定时器、网络和用户事件由浏览器处理,满足条件后把回调排入任务队列;当前任务结束时,浏览器会执行一次微任务检查点,持续清空 Promise、
queueMicrotask等微任务。随后浏览器才有机会执行渲染相关工作,再进入下一个任务。因此常用的理解顺序是“一个任务 → 清空微任务 → 可能渲染 → 下一个任务”,但渲染并不是每轮事件循环都必然发生。
事件循环不是 ECMAScript 语言本身定义的语法能力,而是浏览器宿主环境用来协调以下工作的调度机制:
JavaScript 可以保持单线程的执行模型,同时把等待中的工作交给浏览器处理。异步操作完成并不等于回调立即执行,它只是获得了进入相应队列、等待主线程调度的资格。
同步代码执行的地方。函数调用时入栈,执行结束后出栈。同一时刻,主线程只能执行调用栈顶部的代码。
浏览器负责计时、网络、DOM 事件等工作。例如 setTimeout 会向浏览器注册计时器,而不是让回调在调用栈中等待。
规范通常使用 task;“宏任务”是前端语境中的常见叫法。常见任务来源包括:
setTimeout、setInterval 回调;浏览器可能维护多个任务队列,并根据任务来源和调度策略选择下一个任务,因此不能把所有任务简单理解成唯一的先进先出数组。
常见微任务包括:
Promise.then/catch/finally;queueMicrotask;MutationObserver 回调。当前任务结束后,浏览器会执行微任务检查点,并持续处理微任务,直到队列为空。微任务执行期间新加入的微任务,也会在同一次检查点继续执行。
process.nextTick和setImmediate属于 Node.js 等环境,不应该混入浏览器事件循环题目中。
可以用下面的顺序建立心智模型:
“可能渲染”很重要:浏览器通常会结合刷新率、页面是否可见以及是否有视觉更新来决定是否渲染,并不是执行完每个任务都固定绘制一次。
执行过程如下:
1。setTimeout 注册计时器,回调稍后进入任务队列。Promise 构造函数同步执行,打印 4;.then 回调进入微任务队列。6。5。2,并产生新的 Promise 微任务。3。最终输出:
关键不是记住结果,而是每次都区分:当前正在执行的同步代码、当前任务结束后的微任务,以及后续任务。
微任务会在浏览器获得下一次渲染机会之前清空,因此大量连续微任务可能长期占用主线程:
每个微任务又创建下一个微任务,微任务队列始终无法清空,后续任务和页面渲染都会被推迟。这称为微任务饥饿。
因此,微任务并不是“越多越快”。需要主动让出主线程的长任务,不应该通过递归微任务无限拆分。
requestAnimationFrame(rAF)回调既不是普通任务,也不是微任务。浏览器准备绘制新一帧时,会在样式计算、布局和绘制之前调用本帧的 rAF 回调。
它适合修改下一帧需要展示的视觉状态:
需要注意:
requestIdleCallback 则用于浏览器判断当前帧还有空闲预算时执行低优先级工作,两者的定位不同。
setTimeout(fn, 0) 会立即执行吗?不会。0 表示达到最小延迟后,回调可以进入任务队列。它仍要等待当前任务、当前微任务以及排在前面的其他任务完成,还可能受到嵌套定时器最小延迟和后台页面节流影响。
setTimeout?不是因为 Promise 的计算速度更快,而是因为调度阶段不同:Promise 回调进入微任务队列,会在当前任务结束时执行;定时器回调属于后续任务。
JavaScript 层面的 DOM 属性通常已经改变,但屏幕像素不一定已经更新。浏览器要等主线程交还控制权,并获得合适的渲染机会后,才会完成样式、布局和绘制。
不一样。两者都有任务与微任务概念,但 Node.js 还包含 timers、poll、check 等阶段,并有 process.nextTick 队列。做浏览器题时不应使用 Node 专属 API 推导结果。
nextTick 与事件循环有什么关系?Vue 会批量调度 DOM 更新,并让 nextTick 回调在更新完成后执行。其具体实现与版本有关,但理解微任务检查点有助于理解为什么同步修改状态后,DOM 不一定立刻完成更新。详见 Vue nextTick。