useState 与 requestAnimationFrame 实现的useAnimationFrame
侧边栏壁纸
  • 累计撰写 1,061 篇文章
  • 累计收到 0 条评论

useState 与 requestAnimationFrame 实现的useAnimationFrame

加速器之家
2024-10-20 / 0 评论 / 3 阅读 / 正在检测是否收录...

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

1. 复习 useInterval #

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

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 来实现下进度的变化:

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 来保存:

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 后,就可以使用这个来实现进度的变化:

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 封装一个更加复杂的组件和useProgress的自定义 hook;

3.1 实现 Progress 组件 #

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

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

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

这时,我们就可以编写组件了。

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 绑定几个方法

// 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:

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 只是返回了当前的进度,使用起来也非常地方便:

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 的变化,比较麻烦,这里就不展开讲了。

0

评论

博主关闭了当前页面的评论