JavaScript双requestAnimationFrame(RAF) 科普贴
1、requestAnimationFrame(简称 RAF)是浏览器提供的一个用于同步动画和页面重绘的 API,它可以让代码在浏览器下一次重绘之前执行,从而实现高效、流畅的动画效果。
2、在 JavaScript 中,连续使用两个 requestAnimationFrame(简称 RAF)通常用于处理需要分帧执行的逻辑,避免单帧内执行过多操作导致页面卡顿。
3、双 requestAnimationFrame 的核心作用是分离两部分逻辑到不同帧执行,常用于等待 DOM 渲染完成、拆分重任务、优化动画流畅度等场景。根据具体需求判断是否需要引入这一机制,避免过度使用导致不必要的延迟。
1. 双 RAF 的基本用法
// 第一个 RAF:在下一帧执行回调
requestAnimationFrame(() => {
console.log('第一帧执行');
// 第二个 RAF:在再下一帧执行回调
requestAnimationFrame(() => {
console.log('第二帧执行');
});
});
- 执行时机:第一个回调在当前帧的下一帧触发,第二个回调在第一个回调的下一帧触发,即两者间隔至少一帧(约 16ms,取决于屏幕刷新率)。
2. 为什么需要双 RAF?
主要用于解决浏览器渲染时机相关的问题,常见场景:
场景 1:等待 DOM 样式更新后执行操作
当你修改了 DOM 样式(如 display: none 改为 block),浏览器可能不会立即重绘,此时用单 RAF 可能无法获取最新的布局信息(如 offsetHeight)。双 RAF 可以确保在样式应用并完成重绘后再执行逻辑:
const el = document.getElementById('box');
// 1. 修改样式
el.style.display = 'block';
// 2. 等待样式生效并完成重绘
requestAnimationFrame(() => {
// 第一帧:样式已更新,但可能尚未完成重绘
requestAnimationFrame(() => {
// 第二帧:确保重绘完成,可获取准确的布局信息
console.log('元素高度:', el.offsetHeight);
});
});
场景 2:避免单帧任务过重
如果有两个耗时操作(如下载数据 + 渲染),可拆分到两帧执行,防止单帧卡顿:
// 第一帧处理数据下载
requestAnimationFrame(async () => {
const data = await fetchData();
// 第二帧处理渲染(避免与下载在同一帧阻塞)
requestAnimationFrame(() => {
renderData(data);
});
});
场景 3:解决动画初始状态闪烁
在初始化动画时,双 RAF 可确保元素样式已正确应用,再启动动画:
const el = document.getElementById('animate');
// 设置初始样式
el.style.opacity = '0';
// 等待样式生效后再执行动画
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// 此时 opacity:0 已生效,动画从 0 → 1 更流畅
el.style.transition = 'opacity 0.3s';
el.style.opacity = '1';
});
});
3. 注意事项
- 性能影响:双 RAF 会增加一帧延迟(约 16ms),非必要场景不建议使用。
- 替代方案:部分场景可用 requestPostAnimationFrame(更晚的时机,用于布局读取)或 setTimeout(但时机不如 RAF 精准)。
- 兼容性:所有现代浏览器都支持 requestAnimationFrame,无需担心兼容问题。
4. 双RAF与vue nexttick
Vue 中的 nextTick 与双 requestAnimationFrame 有相似之处(都用于等待 DOM 更新后执行操作),但核心机制和适用场景有所不同。以下是详细对比:
4.1. 机制对比
- nextTick:
Vue 内部的异步更新队列机制,会在当前 DOM 更新周期完成后触发回调。底层优先使用 Promise.then、MutationObserver 等微任务,若不支持则降级为 setTimeout 或 requestAnimationFrame。时机:在 DOM 结构更新完成后(但未必完成渲染绘制)。 - 双 requestAnimationFrame:
基于浏览器的帧渲染机制,第一个回调等待下一帧,第二个回调等待再下一帧。时机:严格与浏览器重绘节奏绑定,确保回调在渲染绘制完成后执行。
4.2. 效果相似性
在等待 DOM 更新后执行操作的场景中,两者常能达到类似效果。例如:
场景:修改数据后获取更新后的 DOM 尺寸
// 使用 nextTick
this.msg = '新内容';
this.$nextTick(() => {
console.log('DOM 已更新,尺寸:', this.$refs.box.offsetHeight);
});
// 使用双 requestAnimationFrame
this.msg = '新内容';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
console.log('DOM 已更新,尺寸:', this.$refs.box.offsetHeight);
});
});两种方式都能获取到数据更新后的 DOM 尺寸,因为它们都等待了 Vue 的 DOM 更新周期。
4.3. 差异与适用场景
特性 | nextTick | 双 requestAnimationFrame |
触发时机 | DOM 更新完成后(微任务阶段) | 两次重绘后(宏任务,与帧率绑定) |
延迟时间 | 通常更短(微任务优先) | 至少两帧(约 32ms,取决于屏幕刷新率) |
核心用途 | 等待 Vue 数据驱动的 DOM 更新 | 等待浏览器渲染绘制完成(如获取布局) |
适用场景 | 数据更新后操作 DOM(如表单聚焦) | 动画同步、精确获取渲染后样式 |
4.4. 何时用nextTick替代双 RAF?
- 当操作依赖于 Vue 数据更新后的 DOM 结构(而非渲染绘制结果)时,nextTick 更高效(延迟更短)。
例如:数据更新后滚动到指定元素、设置焦点等。
// 数据更新后让输入框聚焦(用 nextTick 更合适)
this.showInput = true;
this.$nextTick(() => {
this.$refs.input.focus(); // 确保 input 已渲染
});
4.5. 何时必须用双 RAF?
- 当操作依赖于 浏览器实际渲染结果(如计算元素在屏幕上的实际位置、同步动画帧)时,需要双 RAF。
例如:获取元素的 getBoundingClientRect() 精确值(受 CSS 变换影响时)。 - 示例:
// 确保获取到渲染后的位置信息
this.$refs.box.style.transform = 'translateX(100px)';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
console.log('实际位置:', this.$refs.box.getBoundingClientRect().left);
});
});
总结
- nextTick 是 Vue 为等待自身数据驱动的 DOM 更新设计的,更轻量、延迟更低,优先用于 Vue 生态内的 DOM 操作。
- 双 requestAnimationFrame 更贴近浏览器渲染机制,适合需要等待实际绘制完成的场景(如动画、精确布局计算)。
多数情况下,Vue 中用 nextTick 即可满足 “等待 DOM 更新” 的需求,除非明确依赖渲染绘制结果,否则无需使用双 RAF。