Skip to content

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等静态资源, 上报错误日志等

  1. 点击 "开始计算" 按钮, 开始执行比较耗时的计算
  2. 点击 "改变颜色" 按钮, 看是否能够立即改变颜色, 而不是卡住不响应
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>

Released under the MIT License.