useState 与 requestAnimationFrame 实现的useAnimationFrame

蚊子前端博客
发布于 2020-11-02 12:25
如何使用requestAnimationFrame实现state的变化

我们在之前的文章 如何构建自己的 react hooks 中实现过useInterval的自定义 hook。我们有个进度条,需要从进度 0 逐渐增加到固定的进度时,使用 useInterval 就可以实现。

1. 复习 useInterval

我们再看看之前的useInterval是怎么实现的。

COPYJAVASCRIPT

const useInterval = (callback, delay) => { const saveCallback = useRef(); useEffect(() => { // 每次渲染后,保存新的回调到我们的 ref 里 saveCallback.current = callback; }); useEffect(() => { function tick() { saveCallback.current(); } // delay变为null的的时候,会先清除掉之前的定时器 // 然后也不会起新的定时器,整个useInterval结束 if (delay !== null) { let id = setInterval(tick, delay); return () => clearInterval(id); } }, [delay]); };

使用 useInterval 来实现下进度的变化:

COPYJAVASCRIPT

const progress = 76; // 当前的实际进度 const [step, setStep] = useState(0); // 进度的过程 // useInterval怎么实现的可以查看上面的链接 useInterval( () => { setStep(step + 1); }, step < progress ? 20 : null ); // 当为数字时则按照这个数字切换,若为null则停止定时器

但使用 useInterval 的话,颗粒度不够细,可能就会存在丢帧的情况。这里我们就要考虑到使用requestAnimationFrame来实现进度的一个变化。

2. 实现 useAnimationFrame

我们可以仿照 useInterval 的写法来写一个 useAnimationFrame,但要注意的是,setInterval 是启动之后就不用再管了,他会自动按照间隔来执行下一个任务,但 requestAnimationFrame 类似于 setTimeout,每次都要在当前任务里去启动下一个任务,这样就会产生一个新的 requestId(取消时使用),因此这个新的 requestId 也要用一个 ref 来保存:

COPYJAVASCRIPT

const useAnimationFrame = (callback, running) => { const savedCallback = useRef(callback); // 传进来的callback const requestId = useRef(0); // 当前正在执行的requestId useEffect(() => { savedCallback.current = callback; }); useEffect(() => { function tick() { savedCallback.current(); if (running) { // 当running为true时,才启动下一个,并拿到最新的requestId requestId.current = window.requestAnimationFrame(tick); } } if (running) { const animationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame; const cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame; requestId.current = animationFrame(tick); return () => cancelAnimationFrame(requestId.current); } }, [running]); };

我们实现了 useAnimationFrame 后,就可以使用这个来实现进度的变化:

COPYJAVASCRIPT

const progress = 76; // 当前的实际进度 const [step, setStep] = useState(0); // 进度的过程 useAnimationFrame(() => { setStep(step + 1); }, step < progress); // 这里为true和false

useAnimationFrame的 hook 相比 useInterval,数据变化会更加流畅,点击查看 demo:useAnimationFrame 的使用

3. 基于 useAnimationFrame 实现更多的功能

我们在useAnimationFrame中只是实现了基本的数据变化功能,但实际应用中,还需要更多的功能,例如:

  1. 监听每一步的变化,监听动画结束事件;

  2. 手动启动、暂停进度变化;

  3. 数据按照动画效果进行变化;

  4. 控制进度的快慢 step;

因此,可以基于 useAnimationFrame 封装一个更加复杂的<Progress />组件和useProgress的自定义 hook;

3.1 实现 Progress 组件

我们先定义几个 Progress 组件的属性:

COPYJAVASCRIPT

interface ProgressProps { startNum?: number; // 起始进度 endNum?: number; // 结束的进度 step?: number; // 每一步跳跃的进度 running?: boolean; // 进度是否进行 onStart?: () => void; // 监听开始事件 onStep?: (step: number) => void; // 监听进度变化事件 onEnd?: () => void; // 监听进度结束的事件 }

我们当前组件其实只需要通过一个running字段来控制进度是否前进。

这时,我们就可以编写<Progress />组件了。

COPYJAVASCRIPT

const Progress = ({ startNum = 0, endNum = 100, step = 1, running = false, onStart = () => {}, // 开始的回调 onStep = () => {}, // 每一步的回调 onEnd = () => {}, // 结束时的回调 }: ProgressProps) => { const [progress, setProgress] = useState < number > startNum; onStart(); useAnimationFrame(() => { const nextProgress = Math.min(progress + step, endNum); if (nextProgress <= endNum) { setProgress(nextProgress); onStep(nextProgress); } else { onEnd(); } }, running && progress < endNum); return <p>{progress}</p>; };

如果组件更复杂一些,可以给父级组件暴露几个方法,方便更多的控制,使用useImperativeHandle,给传入的 ref 绑定几个方法

COPYJAVASCRIPT

// cref为传进来的ref对象 useImperativeHandle(cref, () => ({ // 这里是暴露给父组件的方法 // 开始 start() { setRunning(true); }, // 暂停 pause() { setRunning(false); }, // 切换状态 toggle() { setRunning(!running); }, // 重新开始 restart() { setProgress(startNum); setRunning(true); }, }));

点击查看 Progress 组件的使用:Progress 组件的使用

3.2 实现 useProgress 的 hook

若希望更加简洁一些,只想获取当前的进度,可以基于 useAnimationFrame 自定义一个useProgress的 hook:

COPYJAVASCRIPT

const useProgress = ({ startNum = 0, endNum = 100, step = 1, running = true, }) => { const [progress, setProgress] = useState(startNum); useAnimationFrame(() => { const nextProgress = Math.min(progress + step, endNum); setProgress(nextProgress); }, running && progress < endNum); return progress; };

这里的 useProgress 只是返回了当前的进度,使用起来也非常地方便:

COPYJAVASCRIPT

const progress = useProgress({}); return ( <div className="home"> <p>progress: {progress}</p> </div> );

4. 总结

这里我们总结了下 useState 和 requestAnimationFrame 封装的 useAnimationFrame 的 hook,然后通过这个 hook 可以很方便地控制页面中 state 的变化。本来想在 useProgress 中可以根据三次贝塞尔曲线来进行一个进度的曲线变化,但研究了一下发现,三次贝塞尔曲线的坐标(x, y)都是通过变量 t 得到的,这里需要推导出一个 y=f(x)的公式,根据 x 得到 y 的变化,比较麻烦,这里就不展开讲了。

阅读(1063)

公众号:

qrcode

微信公众号:前端小茶馆