24

useState 与 requestAnimationFrame 实现的useAnimationFrame

 3 years ago
source link: https://www.xiabingbao.com/post/react/useanimationframe.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

如何使用requestAnimationFrame实现state的变化

我们在之前的文章 如何构建自己的 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 封装一个更加复杂的 <Progress /> 组件和 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 字段来控制进度是否前进。

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK