请求动画帧 - requestAnimationFrame

420 阅读2分钟

记录一个不常见的API - 请求动画帧

requestAnimationFrame 用法: handlerId = requestAnimationFrame(callback);

window.requestAnimationFrame()  告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。

  • 传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘前执行。
  • 返回值handlerId为浏览器定义的、大于0的整数,唯一标识了该回调函数在列表中位置,用于取消回调函数,有点类似setInterval。
  • 同时: requestAnimationFrame 还是 react 官方团队为了兼容浏览器差异实现 requestIdleCallback(利用浏览器空闲时间来计算,使fiber可以基于优先级计算并获得极高性能原因之一)而设计的ployfill原理之一。

浏览器在执行requestAnimationFrame 发生了什么呢 ?

  • 首先要判断document.hidden属性是否为true,即页面处于可见状态下才会执行;
  • 浏览器清空上一轮的动画函数;
  • 方法返回的handlerId 值会和动画函数callback,以<handlerId , callback>进入到动画帧请求回调函数列;
  • 浏览器会遍历动画帧请求回调函数列表,根据handlerId的值大小,依次去执行相应的动画函数。
  • 注意: requestAnimationFrame() 排序的回调函数被触发的时间。在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。该时间戳是一个十进制数,单位毫秒,最小精度为 1ms(1000μs)。

cancelAnimationFrame 用于取消动画帧函数 用法: cancelAnimationFrame(handlerId);

使用示例

const element = document.getElementById('some-element-you-want-to-animate');
let start, previousTimeStamp;
let done = false

function step(timestamp) {
  if (start === undefined) {
    start = timestamp;
  }
  const elapsed = timestamp - start;

  if (previousTimeStamp !== timestamp) {
    // 这里使用 `Math.min()` 确保元素刚好停在 200px 的位置。
    const count = Math.min(0.1 * elapsed, 200);
    element.style.transform = 'translateX(' + count + 'px)';
    if (count === 200) done = true;
  }

  if (elapsed < 2000) { // 在两秒后停止动画
    previousTimeStamp = timestamp;
    if (!done) {
      window.requestAnimationFrame(step);
    }
  }
}

window.requestAnimationFrame(step);

还可以用来做进度条

<div id="myDiv" style="background-color: lightblue;width: 0;height: 20px;line-height: 20px;">0%</div>
<button id="btn">run</button>
<script>
var timer;
btn.onclick = function(){
    myDiv.style.width = '0';
    cancelAnimationFrame(timer);
    timer = requestAnimationFrame(function fn(){
        if(parseInt(myDiv.style.width) < 500){
            myDiv.style.width = parseInt(myDiv.style.width) + 5 + 'px';
            myDiv.innerHTML =     parseInt(myDiv.style.width)/5 + '%';
            timer = requestAnimationFrame(fn);
        }else{
            cancelAnimationFrame(timer);
        }    
    });
}
</script>

那么这样的API除了可以控制动画外,还可以用来做什么呢?

React ahooks 中 给了我们一个好的启发: React ahooks 中对 setRafState 的封装通过配合useCallback 对useState 做了优化。

const useRafState<S>(initialState?: S | (() => S)) => {
    const ref = useRef(0);
    const [state, setState] = useState(initialState);
    useEffect(() => {
        return {
           cancelAnimationFrame(ref.current);
        }
    }, [])
    const setRefState = useCallback((value: S | ((prevState: S) => S)) => {
        cancelAnimationFrame(ref.current);
        //只在 requestAnimationFrame callback 时更新 state,一般用于性能优化。
        ref.current = requestAnimationFrame(() => {
            setState(value);
        }, []);
    }
    return [state, setRafState] as const;
}
export default useRafState;