Published on

useRafState

Authors
  • avatar
    Name
    李丹秋
    Twitter

requestAnimationFrame定义

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。 requestAnimationFrame()会返回一个数字,没有特殊意义,只是为了后续能够通过cancelAnimationFrame取消回调函数请求。

上面对requestAnimationFrame的定义说是会在重绘之前调用回调函数,但是按照一般的理解,只有操作样式的修改等行为之后,才会触发重绘,这里就产生了比较大的困惑,所以我又去看了一遍浏览器渲染原理。 MDN 浏览器渲染原理

浏览器每一帧

浏览器每一帧

根据上图,可以理解为requestAnimationFrame没一帧都会执行,上面说的重绘指的其实是帧的渲染,和我们常说的重绘不是一个概念,一般说的重绘指的是Paint, 只有在样式发生改变的时候,才会触发。 这篇文章也详细讲解了这个过程,印证了我上面的推测

为什么requestAnimationFrame是对屏幕进行可视更新的理想场所?

  1. 它由浏览器自己调度,因此可以优化性能,避免过度更新。浏览器会在下一帧之前调用回调函数,所以可以避免不必要的重绘和回流。
  2. 它与浏览器的刷新频率(通常每秒60帧)同步,因此可以避免更新丢帧。回调函数会在每一帧被调用,所以动画可以流畅运行。
  3. 它会考虑标签页是否在后台等因素,自动节流以优化性能。当标签页不可见时,回调函数会停止被调用,以节省资源。
  4. 它会在下一次重绘之前执行,因此多个回调可以被规整为一次重绘,提高效率。
  5. 它通过回调的时间戳参数,可以计算每帧的时间,做出响应的动画调整。
  6. 相比 setTimeout/setInterval,它可以更好地优化动画性能,减少卡顿。

useRafState

经过上面对requestAnimationFrame的理解,我们再来看useRafState做了什么处理

源码

import { useCallback, useRef, useState } from 'react';
import type { Dispatch, SetStateAction } from  'react';
import useUnmount from '../useUnmount';

function useRafState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
function useRafState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];

function useRafState<S>(initialState?: S | (() => S)) {
    const ref = useRef(0);
    const [state, setState] = useState(initialState);

    const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
        cancelAnimationFrame(ref.current);

        ref.current = requestAnimationFrame(() => {
            setState(value);
        });
    }, []);

    useUnmount(() => {
        cancelAnimationFrame(ref.current);
    });

    return [state, setRafState] as const;
}

export default useRafState;

示例

import { useRafState } from 'ahooks';
import React, { useEffect } from 'react';

export default () => {
  const [state, setState] = useRafState({
    width: 0,
    height: 0,
  });

  useEffect(() => {
    const onResize = () => {
      setState({
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight,
      });
    };
    onResize();

    window.addEventListener('resize', onResize);

    return () => {
      window.removeEventListener('resize', onResize);
    };
  }, []);

  return (
    <div>
      <p>Try to resize the window </p>
      current: {JSON.stringify(state)}
    </div>
  );
};

只在 requestAnimationFrame callback 时更新 state,一般用于性能优化。

它内部有个 setRafState 函数,该函数通过 requestAnimationFrame 去执行 setState,它执行的时候,会取消上一次的 setRafState 操作。然后重新通过 requestAnimationFrame 去控制本次 setState 的执行时机。然后在页面卸载的时候(useUnmount)的时候,也会取消操作。这样是为了防止内存泄漏(和定时器一个道理)