hook实现阻止功能_useState与requestAnimationFrame实现useAnimationFrame

d64ed07c78ed8dbd60e967f852329bd8.png

欢迎关注我的博客哦:

useState 与 requestAnimationFrame 实现的useAnimationFrame-蚊子的前端博客​www.xiabingbao.com
0064655b9cc58b1ac1ab464e8216c5e6.png

我们在之前的文章 如何构建自己的 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 的变化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值