html 结构和样式
html
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="no-referrer" />
<title>requestAnimationFame</title>
<style>
* {
margin: 0;
padding: 0;
}
#app {
position: relative;
}
#box {
width: 100px;
height: 100px;
background: #f00;
position: absolute;
}
</style>
</head>
<body>
<button id="start">start</button>
<button id="stop">stop</button>
<div id="app">
<div id="box"></div>
</div>
<script src="./index.js" type="module"></script>
</body>
</html>setInterval/setTimeout
这是传统的JS动画做法: 优点是 API 兼容性好, 简单直观, 但是缺点也很明显:
- 不考虑浏览器渲染周期, 只关注时间延迟
- 即使页面在后台或不可见, 计时器仍会运行
- 无论显示器刷新率如何或页面性能状况,都按照固定间隔执行
- 在一帧渲染后执行,但下一帧前无法再次渲染(掉帧)
js
const boxDom = document.getElementById("box");
const startBtn = document.getElementById("start");
const stopBtn = document.getElementById("stop");
let timer;
const position = {
left: 0,
top: 0,
};
function setBoxDomPos(position) {
boxDom.style.left = `${position.left}px`;
boxDom.style.top = `${position.top}px`;
}
function startAnimation() {
timer && clearInterval(timer);
timer = setInterval(() => {
position.left += 5;
position.top += 5;
setBoxDomPos(position);
}, 17);
}
function stopAnimation() {
timer && clearInterval(timer);
}
window.onload = () => {
startBtn.addEventListener("click", () => {
startAnimation();
});
stopBtn.addEventListener("click", () => {
stopAnimation();
});
};js
const boxDom = document.getElementById("box");
const startBtn = document.getElementById("start");
const stopBtn = document.getElementById("stop");
let timer;
const position = {
left: 0,
top: 0,
};
function setBoxDomPos(position) {
boxDom.style.left = `${position.left}px`;
boxDom.style.top = `${position.top}px`;
}
function startAnimation() {
timer && clearTimeout(timer);
timer = setTimeout(function run() {
position.left += 5;
position.top += 5;
setBoxDomPos(position);
timer = setTimeout(run, 17);
}, 17);
}
function stopAnimation() {
timer && clearTimeout(timer);
}
window.onload = () => {
startBtn.addEventListener("click", () => {
startAnimation();
});
stopBtn.addEventListener("click", () => {
stopAnimation();
});
};requestAnimationFrame/cancelAnimation
可以将这个API看作 "专为动画而设计的智能版setTimeout"
- 专为动画而设计,与浏览器渲染机制深度集成
- 优化视觉流畅度,确保动画与屏幕刷新同步
- 在浏览器下一次重绘前执行
- 与显示器刷新率同步, 浏览器会合并多个rAF回调到一帧
- 当标签在后台或不可见时: 会自动暂停执行
js
const boxDom = document.getElementById("box");
const startBtn = document.getElementById("start");
const stopBtn = document.getElementById("stop");
let rafId;
const position = {
left: 0,
top: 0,
};
function setBoxDomPos(position) {
boxDom.style.left = `${position.left}px`;
boxDom.style.top = `${position.top}px`;
}
function startAnimation() {
rafId && cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(function run() {
position.left += 5;
position.top += 5;
setBoxDomPos(position);
rafId = requestAnimationFrame(run);
});
}
function stopAnimation() {
rafId && cancelAnimationFrame(rafId);
}
window.onload = () => {
startBtn.addEventListener("click", () => {
startAnimation();
});
stopBtn.addEventListener("click", () => {
stopAnimation();
});
};扩展: requestIdleCallback/cancelIdleCallback
虽然两者都是浏览器提供的调度 API, 但它们的设计目标、执行时机和适用场景完全不同
| API | 优先级 | 设计目的 | 调度时机 |
|---|---|---|---|
| requestAnimationFrame(rAF) | 高 | "快点执行,下帧就要用!" | 在下一帧渲染前执行高优先级任务(如动画), 确保视觉流畅 |
| requestIdleCallback(rIC) | 低 | "有空再执行, 不着急" | 在浏览器空闲时执行低优先级、非关键任务(如日志上报、预加载css等静态资源), 避免影响主线程性能 |
实际应用
当主线程在执行高强度计算时, 会导致浏览器卡顿, 一些不重要的低优先级任务 就可以放到 "在浏览器空闲时执行", 比如: 预加载css等静态资源, 上报错误日志等
- 点击 "开始计算" 按钮, 开始执行比较耗时的计算
- 点击 "改变颜色" 按钮, 看是否能够立即改变颜色, 而不是卡住不响应
js
const boxDom = document.getElementById("box");
const btn1Dom = document.getElementById("btn1");
const btn2Dom = document.getElementById("btn2");
// 斐波那契数列
function fabonacci(n) {
if (n === 0) {
return 0;
} else if (n === 1) {
return 1;
} else {
return fabonacci(n - 1) + fabonacci(n - 2);
}
}
// 获取随机颜色字符串 #000000 - #ffffff
function getRandomColor() {
return "#" + Math.floor(Math.random() * 0xffffff).toString(16);
}
let count = 99999;
function calc(deadline) {
// deadline.timeRemaining() 返回剩余(获取浏览器空余时间)
// deadline.didTimeout 返回是否超时(第二个参数设置的 timeout 时间)
while (count > 0 && (deadline.timeRemaining() > 1 || deadline.didTimeout)) {
const random = Math.floor(Math.random() * 10);
const result = fabonacci(random);
console.log(`${count}: ${random} 计算斐波那契数列的结果: ${result}`);
count--;
}
requestIdleCallback(calc, { timeout: 500 });
}
// 计算任务: 耗时但并不是很重要的任务
btn1Dom.addEventListener("click", () => {
requestIdleCallback(calc, { timeout: 500 });
});
// 用户交互: 应该保证用户能够流程操作不卡顿
btn2Dom.addEventListener("click", () => {
boxDom.style.background = getRandomColor();
});js
const boxDom = document.getElementById("box");
const btn1Dom = document.getElementById("btn1");
const btn2Dom = document.getElementById("btn2");
// 斐波那契数列
function fabonacci(n) {
if (n === 0) {
return 0;
} else if (n === 1) {
return 1;
} else {
return fabonacci(n - 1) + fabonacci(n - 2);
}
}
// 获取随机颜色字符串 #000000 - #ffffff
function getRandomColor() {
return "#" + Math.floor(Math.random() * 0xffffff).toString(16);
}
let count = 99999;
function calc() {
while (count > 0) {
const random = Math.floor(Math.random() * 10);
const result = fabonacci(random);
console.log(`${random} 计算斐波那契数列的结果: ${result}`);
count--;
}
}
// 计算任务: 耗时但并不是很重要的任务
btn1Dom.addEventListener("click", calc);
// 用户交互: 应该保证用户能够流程操作不卡顿
btn2Dom.addEventListener("click", () => {
boxDom.style.background = getRandomColor();
});html
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="no-referrer" />
<title>requestAnimationFame</title>
<style>
#box {
width: 100px;
height: 100px;
background: #f00;
}
</style>
</head>
<body>
<div id="app">
<button id="btn1">开始计算</button>
<button id="btn2">改变颜色</button>
<div id="box"></div>
</div>
<script src="./index.js" type="module"></script>
</body>
</html>