前言
前端项目里使用延时器是正常的,模仿一下异步操作,但是在复杂的业务场景下不建议使用,主要有两点,第一js是单线程,第二是官方提供的api足以解决各种执行队列。
你设置的延时器为1,不一定是在1ms后执行
1. 现在各大浏览器的引擎不足以支撑你的需求:
Chrome/Edge:4ms
Firefox:4ms
Safari:4-5ms
2. js是一个单线程的操作,是有事件循环的
javascript
console.log('开始');
setTimeout(() => console.log('延时器'), 1);
console.log('结束');
// 输出顺序:开始 → 结束 → 延时器
即使延时器到期,也要等待当前执行栈清空:
javascript
// 即使延时器设置为0,也要等循环执行完
setTimeout(() => console.log('延时器'), 1);
for(let i = 0; i < 10000; i++) {
console.log(i)
}
nexttick也是用延时器,为啥不一样呢?
Vue 中的 nextTick
底层确实可能用到计时器(如 setTimeout
或 setImmediate
),但它并非简单依赖 setTimeout(fn, 0)
,而是经过了多层优化的 "微任务优先" 方案 ,因此能避免 setTimeout(0)
的大部分缺点,保证可靠性。
一、nextTick
的底层实现:并非单纯依赖计时器
Vue 的 nextTick
核心目的是等待 DOM 更新完成后执行回调 (因为 Vue 的 DOM 更新是异步的)。其实现逻辑是 "优先使用微任务,降级使用宏任务",具体步骤如下:
-
优先尝试微任务:微任务的执行时机在 "当前同步代码执行完毕后、DOM 渲染前",且延迟远小于宏任务(通常 < 1ms),最适合处理 DOM 更新后的回调。Vue 会按优先级尝试以下微任务 API:
Promise.then
(浏览器普遍支持,优先级最高);MutationObserver
(监听 DOM 变化的 API,本质是微任务)。
-
微任务不支持时,降级使用宏任务:若环境不支持微任务(如 IE 浏览器),则会使用宏任务,顺序为:
setImmediate
(仅 IE 和 Node.js 支持,延迟比setTimeout
更稳定);- 最后才会使用
setTimeout(fn, 0)
(兼容性最好,但延迟最高)。
nextTick
虽然底层可能用到 setTimeout
,但它是针对 Vue 异步 DOM 更新机制设计的专用方案 :通过 "微任务优先" 保证低延迟,通过 "任务合并" 减少性能损耗,通过 "绑定 DOM 更新队列" 保证时机精准。这些优化让它完全避开了 setTimeout(0)
的缺点,成为 Vue 中处理 DOM 异步更新的可靠方案。
简单说:nextTick
是 "经过特殊设计的计时器用法",而 setTimeout(0)
是 "通用但粗糙的异步化工具",二者不可同日而语。
页面的刷新频率虽然会造成影响吗?,
有些伙伴觉得,页面的刷新频率导致不能正确的展示出延时器的输出结果,是刷新频率延误了输出时长。实际上这块跟浏览器的渲染关系不是很大
那它是怎么影响的呢?
1. 重绘与回流:页面渲染阻塞 JavaScript 执行
- 重绘与回流:回流是计算页面布局的几何变化,重绘是将像素画到屏幕上。它们共同构成了"渲染"步骤。
- 阻塞效应 :当浏览器进入渲染阶段(包括样式计算、布局、绘制),主线程被占用,此时即使延时器的回调已经到了执行时间,也必须等待整个渲染步骤完成。这就是最直接的"渲染阻塞 JavaScript 执行"。
- 举个例子:假设一个复杂的动画或大量的 DOM 操作触发了回流,这个渲染过程可能耗时 5-10ms。在这期间,你的 1ms 延时器回调只能干等着。
2. 渲染时机:浏览器在下一次重绘前执行积压的任务
这涉及到 setTimeout
与 requestAnimationFrame
的区别。
- **
setTimeout
:它只关心时间,不关心屏幕刷新。它可能在 两次屏幕刷新的中间时刻执行。如果它的执行过程中修改了样式,浏览器不得不紧急进行样式计算和布局,可能导致当前帧无法完成,或者直接将改动推迟到下一帧去渲染,造成丢帧**现象。 - **
requestAnimationFrame
:它的回调函数被设计在每一次浏览器渲染之前、样式计算之后**执行。这是更新动画、修改样式的最佳时机,能确保修改的样式在紧接着的渲染中被绘制出来,从而保证流畅性。
因此,事件循环中积压的 setTimeout
回调,通常会被安排在当前帧的渲染开始之前执行。如果这些回调执行时间过长,挤占了原本属于渲染的时间,就会导致渲染延迟,用户会觉得页面"卡顿"。
3. 页面加载:如果页面还在加载或渲染,延时器会进一步延迟
- 解析阻塞 :浏览器在加载页面时,需要解析 HTML、构建 DOM 树、加载并执行 CSS 和 JavaScript。特别是遇到
<script>
标签(没有async
或defer
属性)时,它会停止 HTML 解析,立即下载并执行该脚本。这个过程会严重阻塞事件循环。 - 举个例子 :你的
setTimeout
代码可能在页面加载初期就被执行了。但此时,主线程正忙于解析一个巨大的 HTML 文档或执行一个庞大的初始化脚本。你的延时器回调必须等所有这些"正事"干完后,才有机会执行。 - 初始渲染:浏览器会尽快进行首次渲染(例如,渲染出部分文字和背景)。这个首次布局和绘制的开销很大,会进一步推迟你那个"微不足道"的 1ms 延时器。