一、先理清核心概念
在讲解执行顺序前,先明确几个关键概念:
- 宏任务(Macrotask) :常见的有
setTimeout、setInterval、I/O 操作、script 整体代码、UI 渲染(注意:渲染是独立阶段,不是宏任务,但和 rAF 强相关)。 - 微任务(Microtask) :
Promise.then/catch/finally、queueMicrotask、MutationObserver等,会在宏任务执行完后、渲染 / 下一个宏任务前立即执行。 - requestAnimationFrame :不属于宏任务 / 微任务,是浏览器专门为动画设计的 API,会在浏览器重绘(渲染)之前执行,执行时机在微任务之后、宏任务之前(下一轮)。
二、事件循环的执行流程
一个完整的事件循环周期执行顺序:
markdown
1. 执行当前宏任务(如 script 主代码)
2. 执行所有微任务(微任务队列清空)
3. 执行 requestAnimationFrame 回调
4. 浏览器进行 UI 渲染(重绘/回流)
5. 取出下一个宏任务执行,重复上述流程
三、代码分析
代码执行优先级:同步代码 > 微任务 > rAF(当前帧) > 普通宏任务(setTimeout) > rAF(下一帧) > 后续普通宏任务。
场景 1:基础顺序(script + 微任务 + rAF + 宏任务)
javascript
// 1. 同步代码(属于第一个宏任务:script 整体)
console.log('同步代码执行');
// 微任务
Promise.resolve().then(() => {
console.log('微任务执行');
});
// requestAnimationFrame
requestAnimationFrame(() => {
console.log('requestAnimationFrame 执行');
});
// 宏任务(setTimeout 是宏任务)
setTimeout(() => {
console.log('setTimeout 宏任务执行');
}, 0);
// 执行结果顺序大部分情况下是这样的:
// 同步代码执行
// 微任务执行
// requestAnimationFrame 执行
// setTimeout 宏任务执行
代码解释1:
- 第一步:执行同步代码,打印「同步代码执行」;
- 第二步:微任务队列有
Promise.then,执行并打印「微任务执行」; - 第三步:浏览器准备渲染前,执行 rAF 回调,打印「requestAnimationFrame 执行」;
- 第四步:浏览器完成渲染后,取出下一个宏任务(setTimeout)执行,打印「setTimeout 宏任务执行」。
代码解释2:
-
正常浏览器环境(60Hz 屏幕,无阻塞) :输出顺序是按上方写的先后顺序执行的:
arduino同步代码执行 微任务执行 requestAnimationFrame 执行 setTimeout 宏任务执行原因:浏览器每 16.7ms 刷新一次,
requestAnimationFrame会在下一次重绘前 执行,而setTimeout即使设为 0,也会有 4ms 左右的最小延迟(浏览器限制),所以requestAnimationFrame先执行。 -
极端情况(主线程阻塞 / 浏览器刷新延迟) :可能出现顺序互换:
arduino同步代码执行 微任务执行 setTimeout 宏任务执行 requestAnimationFrame 执行原因 :如果主线程处理完微任务后,
requestAnimationFrame的回调还没到执行时机(比如浏览器还没到重绘节点),但setTimeout的最小延迟已到,就会先执行setTimeout。
总结
-
固定顺序:同步代码 → 微任务,这两步是绝对固定的,不受任何因素影响。
-
不固定顺序 :
requestAnimationFrame和setTimeout的执行先后不绝对,前者优先级更高但依赖渲染时机,后者受最小延迟限制,多数场景下前者先执行,但不能当作 "绝对结论"。 -
核心原则:
requestAnimationFrame属于 "渲染相关回调",优先级高于普通宏任务(如setTimeout),但并非 ECMAScript 标准定义的 "微任务 / 宏任务" 范畴,而是浏览器的扩展机制,因此执行时机存在微小不确定性。
场景 2:嵌套场景(rAF 内嵌套微任务 / 宏任务)
javascript
console.log('同步代码');
// 第一个 rAF
requestAnimationFrame(() => {
console.log('rAF 1 执行');
// rAF 内的微任务
Promise.resolve().then(() => {
console.log('rAF 1 内的微任务');
});
// rAF 内的宏任务
setTimeout(() => {
console.log('rAF 1 内的 setTimeout');
}, 0);
// rAF 内嵌套 rAF
requestAnimationFrame(() => {
console.log('rAF 2 执行');
});
});
// 外层微任务
Promise.resolve().then(() => {
console.log('外层微任务');
});
// 外层宏任务
setTimeout(() => {
console.log('外层 setTimeout');
}, 0);
// 执行结果顺序:
// 同步代码
// 外层微任务
// rAF 1 执行
// rAF 1 内的微任务
// 外层 setTimeout
// (浏览器下一次渲染前)
// rAF 2 执行
// rAF 1 内的 setTimeout
代码解释:
- 先执行同步代码 → 外层微任务;
- 执行 rAF 1 → 立即执行 rAF 1 内的微任务(微任务会在当前阶段清空);
- 浏览器渲染后,执行下一轮宏任务:外层 setTimeout;
- 下一次事件循环的渲染阶段,执行嵌套的 rAF 2;
- 最后执行 rAF 1 内的 setTimeout(下下轮宏任务)。
场景 3:rAF 与多个宏任务对比
javascript
// 宏任务1:setTimeout 0
setTimeout(() => {
console.log('setTimeout 1');
}, 0);
// rAF
requestAnimationFrame(() => {
console.log('rAF 执行');
});
// 宏任务2:setTimeout 0
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
// 执行结果顺序:
// rAF 执行
// setTimeout 1
// setTimeout 2
结论:即使多个宏任务排在前面,rAF 依然会在「微任务后、渲染前」优先执行,然后才执行所有待处理的宏任务。
四、实际应用
rAF 的这个执行特性,常用来做高性能动画(比如 DOM 动画),因为它能保证在渲染前执行,避免「布局抖动」:
ini
// 用 rAF 实现平滑移动动画
const box = document.getElementById('box');
let left = 0;
function moveBox() {
left += 1;
box.style.left = `${left}px`;
// 动画未结束则继续调用 rAF
if (left < 300) {
requestAnimationFrame(moveBox);
}
}
// 启动动画
requestAnimationFrame(moveBox);
这个代码的优势:rAF 会和浏览器的刷新频率(通常 60Hz,每 16.7ms 一次)同步,不会像 setTimeout 那样可能出现丢帧,因为 setTimeout 是宏任务,执行时机不固定,可能错过渲染时机。
总结
- 核心执行顺序:同步代码 → 所有微任务 → requestAnimationFrame → 浏览器渲染 → 下一轮宏任务(setTimeout/setInterval 等)。
- rAF 本质:不属于宏 / 微任务,是浏览器渲染阶段的「专属回调」,优先级高于下一轮宏任务。
- 实战价值:rAF 适合做 UI 动画,能保证动画流畅;宏任务(setTimeout)适合非渲染相关的异步操作,避免阻塞渲染。
相比传统的计时器防抖与节流
实战代码:rAF 实现节流(最常用)
rAF 做节流的核心优势:和浏览器渲染同步,不会出现「执行次数超过渲染帧」的无效执行,尤其适合 resize、scroll、mousemove 这类和 UI 相关的高频事件。
基础版 rAF 节流
javascript
function rafThrottle(callback) {
let isPending = false; // 标记是否已有待执行的回调
return function(...args) {
if (isPending) return; // 已有待执行任务,直接返回
isPending = true;
// 绑定 this 指向,传递参数
const context = this;
requestAnimationFrame(() => {
callback.apply(context, args); // 执行回调
isPending = false; // 执行完成后重置标记
});
};
}
// 测试:监听滚动事件
window.addEventListener('scroll', rafThrottle(function(e) {
console.log('滚动节流执行', window.scrollY);
}));
代码解释:
isPending标记是否有 rAF 回调待执行,避免同一帧内多次触发;- 每次触发事件时,若没有待执行任务,就通过 rAF 注册回调;
- rAF 会在下一次渲染前执行回调,执行完后重置标记,确保每帧只执行一次。
对比传统 setTimeout 节流
ini
// 传统 setTimeout 节流(对比用)
function timeoutThrottle(callback, delay = 16.7) {
let timer = null;
return function(...args) {
if (timer) return;
timer = setTimeout(() => {
callback.apply(this, args);
timer = null;
}, delay);
};
}
rAF 节流的优势:
- 执行时机和浏览器渲染帧完全同步,不会出现「回调执行了但渲染没跟上」的无效操作;
- 无需手动设置延迟(如 16.7ms),自动适配浏览器刷新率(60Hz/144Hz 都能兼容)。
实战代码:rAF 实现防抖
rAF 实现防抖需要结合「延迟 + 取消 rAF」的逻辑,核心是「触发事件后,只保留最后一次 rAF 回调」。
javascript
function rafDebounce(callback) {
let rafId = null; // 保存 rAF 的 ID,用于取消
return function(...args) {
const context = this;
// 若已有待执行的 rAF,先取消
if (rafId) {
cancelAnimationFrame(rafId);
}
// 重新注册 rAF,延迟到下一帧执行
rafId = requestAnimationFrame(() => {
callback.apply(context, args);
rafId = null; // 执行后清空 ID
});
};
}
// 测试:监听输入框输入
const input = document.getElementById('input');
input.addEventListener('input', rafDebounce(function(e) {
console.log('输入防抖执行', e.target.value);
}));
代码解释:
- 每次触发事件时,先通过
cancelAnimationFrame取消上一次未执行的 rAF 回调; - 重新注册新的 rAF 回调,确保只有「最后一次触发」的回调会执行;
- 防抖的延迟本质是「一帧的时间(16.7ms)」,若需要更长延迟,可结合
setTimeout。
带自定义延迟的 rAF 防抖
ini
function rafDebounceWithDelay(callback, delay = 300) {
let rafId = null;
let timer = null;
return function(...args) {
const context = this;
// 取消之前的定时器和 rAF
if (timer) clearTimeout(timer);
if (rafId) cancelAnimationFrame(rafId);
// 先延迟,再用 rAF 执行(保证渲染前执行)
timer = setTimeout(() => {
rafId = requestAnimationFrame(() => {
callback.apply(context, args);
rafId = null;
timer = null;
});
}, delay);
};
}
四、适用场景 vs 不适用场景
| 场景 | 是否适合用 rAF 做防抖 / 节流 | 原因 |
|---|---|---|
| scroll/resize 事件 | ✅ 非常适合 | 和 UI 渲染强相关,rAF 保证每帧只执行一次 |
| mousemove/mouseover 事件 | ✅ 适合 | 高频触发,rAF 减少无效执行,提升性能 |
| 输入框 input/change 事件 | ✅ 适合(防抖) | 保证输入完成后,在渲染前执行回调(如搜索联想) |
| 网络请求(如按钮点击提交) | ❌ 不适合 | 网络请求和 UI 渲染无关,用传统 setTimeout 防抖更合适 |
| 后端数据处理(无 UI 交互) | ❌ 不适合 | rAF 是浏览器 API,Node.js 环境不支持,且无渲染需求 |
总结
- rAF 适合做防抖 / 节流,尤其在「和 UI 交互相关的高频事件」(scroll/resize/mousemove)场景下,性能优于传统 setTimeout;
- rAF 节流 :核心是「每帧只执行一次」,利用
isPending标记避免重复执行; - rAF 防抖:核心是「取消上一次 rAF,保留最后一次」,可结合 setTimeout 实现自定义延迟;
- 非 UI 相关的防抖 / 节流(如网络请求),优先用传统 setTimeout,避免依赖浏览器渲染机制。