- Published on
useRafState
- Authors

- Name
- 李丹秋
requestAnimationFrame定义
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。 requestAnimationFrame()会返回一个数字,没有特殊意义,只是为了后续能够通过cancelAnimationFrame取消回调函数请求。
上面对requestAnimationFrame的定义说是会在重绘之前调用回调函数,但是按照一般的理解,只有操作样式的修改等行为之后,才会触发重绘,这里就产生了比较大的困惑,所以我又去看了一遍浏览器渲染原理。 MDN 浏览器渲染原理
浏览器每一帧

根据上图,可以理解为requestAnimationFrame没一帧都会执行,上面说的重绘指的其实是帧的渲染,和我们常说的重绘不是一个概念,一般说的重绘指的是Paint, 只有在样式发生改变的时候,才会触发。 这篇文章也详细讲解了这个过程,印证了我上面的推测
为什么requestAnimationFrame是对屏幕进行可视更新的理想场所?
- 它由浏览器自己调度,因此可以优化性能,避免过度更新。浏览器会在下一帧之前调用回调函数,所以可以避免不必要的重绘和回流。
- 它与浏览器的刷新频率(通常每秒60帧)同步,因此可以避免更新丢帧。回调函数会在每一帧被调用,所以动画可以流畅运行。
- 它会考虑标签页是否在后台等因素,自动节流以优化性能。当标签页不可见时,回调函数会停止被调用,以节省资源。
- 它会在下一次重绘之前执行,因此多个回调可以被规整为一次重绘,提高效率。
- 它通过回调的时间戳参数,可以计算每帧的时间,做出响应的动画调整。
- 相比 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)的时候,也会取消操作。这样是为了防止内存泄漏(和定时器一个道理)