关于JS事件循环与浏览器渲染阻塞的实验疑问求解
实验背景
观看《What the heck is the event loop anyway?》和《Web browser event loop, setTimeout, micro tasks, requestAnimationFrame...》后,通过四个实验探究不同延迟任务对浏览器渲染的阻塞情况,以下是各实验的行为解析及疑问解答。
各实验行为解析
1. 同步长任务(delayLoop)
你的理解完全正确:同步任务会直接占用主线程调用栈,在任务执行完毕前调用栈不会空闲,浏览器无法处理渲染任务队列,因此UI会全程冻结直到任务结束。
2. Promise回调执行长任务(delayPromise)
你的认知没有问题:Promise.resolve().then()的回调属于微任务,微任务会在当前宏任务执行完毕后、下一个宏任务启动前全部执行完毕。当func这个宏任务执行完成后,会立即执行该微任务内的长循环,此时调用栈被占用,浏览器依然无法插入渲染任务,因此渲染会被延迟到微任务执行完成后。
3. 新建Promise时在resolve前执行长任务(delayPromiseReturn)
你疑惑的核心误区在于:Promise的executor函数(即new Promise(resolve => { ... })中的回调)是同步执行的。
当你在func中调用delayPromiseReturn()时,函数会立即创建Promise,同时同步执行executor内的所有代码:先打印starting await,然后执行长循环,循环结束后打印ending await,最后调用resolve()——整个过程完全同步,会持续占用调用栈直到全部完成。
也就是说,delayPromiseReturn()的执行是同步阻塞的,它会卡住func后续代码的执行:只有当executor里的长循环跑完,func才会继续执行console.log('ending delay func')。这和Promise回调的异步性无关,executor本身就是同步执行的逻辑。
4. setTimeout回调执行长任务(delayTimeout)
这个超出预期的行为,核心在于浏览器的任务调度机制与合成线程独立运行:
setTimeout的回调属于宏任务,会被加入宏任务队列,在指定延迟(此处5秒)后等待主线程空闲时执行。- 你写的动画是
transform: rotate,这属于合成层动画——这类动画由浏览器的合成线程独立处理,不需要主线程参与(只有涉及DOM重排/重绘的动画才会依赖主线程)。
点击按钮后,func作为宏任务执行完毕,主线程空闲,浏览器立即处理渲染任务,启动动画。5秒后setTimeout的回调进入宏任务队列并开始执行长循环,此时主线程被阻塞,但合成线程不受影响,依然能正常完成动画渲染,因此你会看到动画正常执行,而setTimeout的回调在主线程后台跑完后才打印日志。
实验代码
JavaScript
function delayLoop() { console.log('starting for loop') for(let i = 0; i < 10000000000; i++) {} console.log('ending for loop') } function delayPromise() { Promise.resolve().then(function() { console.log('starting promise') for(let i = 0; i < 10000000000; i++) {} console.log('ending promise') }) } function delayPromiseReturn() { return new Promise(function(resolve) { console.log('starting await') for(let i = 0; i < 10000000000; i++) {} console.log('ending await') resolve() }) } function delayTimeout() { setTimeout(function() { console.log('starting timeout') for(let i = 0; i < 10000000000; i++) {} console.log('ending timeout') }, 5000) } function func() { console.log('starting delay func') document.querySelector('.anim').classList.add('start') /* delayLoop() */ /* delayPromise() */ /* delayPromiseReturn() */ delayTimeout() console.log('ending delay func') }
CSS
.anim { height: 50px; width: 50px; background-color: blue; } @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(90deg); } } .start { animation: rotation 1s linear 10; }
HTML
<button onclick="func()">Click</button> <div class="anim"></div>
内容的提问来源于stack exchange,提问作者Devansh Sharma

