这个CountTo组件npmjs里当然有大把的依赖存在,不过今天我们不需要借助任何三方依赖,造个轮子来手动实现这个组件。
通过研究其他count to插件我们可以发现,数字滚动效果主要依赖于requestAnimationFrame
通过js帧来让数字动起来,数字变化则是依赖于内部的easingFn
函数来每次计算。
首先声明组件props类型
interface Props {
/**
* 动画开始的值
*/
start?: number;
/**
* 目标值
*/
end: number;
/**
* 持续时间
*/
duration?: number;
/**
* 是否自动播放
*/
autoPlay?: boolean;
/**
* 精度
*/
decimals?: number;
/**
* 小数点
*/
decimal?: string;
/**
* 千分位分隔符
*/
separator?: string;
/**
* 数字前 额外信息
*/
prefix?: string;
/**
* 数字后 额外信息
*/
suffix?: string;
/**
* 是否使用变速函数
*/
useEasing?: boolean;
/**
* 计算函数
*/
easingFn?: (t: number, b: number, c: number, d: number) => number;
/**
* 动画开始后传给父组件的回调
*/
started?: () => void;
/**
* 动画结束传递给父组件的回调
*/
ended?: () => void;
}
除了end
是必要的,其他都是可选参数。
所以我们需要给组件默认值,防止没有参数时会报错。
同时写几个工具函数便于后面使用
export default function Index({
end,
start = 0,
duration = 3000,
autoPlay = true,
decimals = 0,
decimal = '.',
separator = ',',
prefix = '',
suffix = '',
useEasing = true,
easingFn = (t, b, c, d) => (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b,
started = () => {},
ended = () => {},
}: Props) {
const isNumber = (val: string) => {
return !isNaN(parseFloat(val));
};
// 格式化数据,返回想要展示的数据格式
const formatNumber = (n: number) => {
let val = '';
if (n % 1 !== 0) val = n.toFixed(decimals);
const x = val.split('.');
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : '';
const rgx = /(\d+)(\d{3})/;
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + separator + '$2');
}
}
return prefix + x1 + x2 + suffix;
};
...
}
初始化数据
const [state, setState] = useState<State>({
start: 0,
paused: false,
duration,
});
const startTime = useRef(0);
const _timestamp = useRef(0);
const remaining = useRef(0);
const printVal = useRef(0);
const rAf = useRef(0);
const endRef = useRef(end);
const endedCallback = useRef(ended);
const [displayValue, setValue] = useState(formatNumber(start));
// 定义一个计算属性,当开始数字大于结束数字时返回true
const stopCount = useMemo(() => start > end, [start, end]);
动画的关键函数
const count = (timestamp: number) => {
if (!startTime.current) startTime.current = timestamp;
_timestamp.current = timestamp;
const progress = timestamp - startTime.current;
remaining.current = state.duration - progress;
// 是否使用速度变化曲线
if (useEasing) {
if (stopCount) {
printVal.current = state.start - easingFn(progress, 0, state.start - end, state.duration);
} else {
printVal.current = easingFn(progress, state.start, end - state.start, state.duration);
}
} else {
if (stopCount) {
printVal.current = state.start - (state.start - endRef.current) * (progress / state.duration);
} else {
printVal.current = state.start + (endRef.current - state.start) * (progress / state.duration);
}
}
if (stopCount) {
printVal.current = printVal.current < endRef.current ? endRef.current : printVal.current;
} else {
printVal.current = printVal.current > endRef.current ? endRef.current : printVal.current;
}
setValue(formatNumber(printVal.current));
if (progress < state.duration) {
rAf.current = requestAnimationFrame(count);
} else {
endedCallback.current?.();
}
};
执行动画的函数
const startCount = () => {
setState({ ...state, start, duration, paused: false });
rAf.current = requestAnimationFrame(count);
startTime.current = 0;
};
挂载时监听是否有autoPlay
来选择是否开始动画,同时组件销毁后清除requestAnimationFrame
动画;
useEffect(() => {
if (autoPlay) {
startCount();
started?.();
}
return () => {
cancelAnimationFrame(rAf.current);
};
}, []);
一些相关依赖的监听及处理
useEffect(() => {
if (!autoPlay) {
cancelAnimationFrame(rAf.current);
setState({ ...state, paused: true });
}
}, [autoPlay]);
useEffect(() => {
if (!state.paused) {
cancelAnimationFrame(rAf.current);
startCount();
}
}, [start]);
最后返回displayValue
就可以了;
好了 我要开启五一假期了!
最后附上完整代码 –
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
interface Props {
/**
* 动画开始的值
*/
start?: number;
/**
* 目标值
*/
end: number;
/**
* 持续时间
*/
duration?: number;
/**
* 是否自动播放
*/
autoPlay?: boolean;
/**
* 精度
*/
decimals?: number;
/**
* 小数点
*/
decimal?: string;
/**
* 千分位分隔符
*/
separator?: string;
/**
* 数字前 额外信息
*/
prefix?: string;
/**
* 数字后 额外信息
*/
suffix?: string;
/**
* 是否使用变速函数
*/
useEasing?: boolean;
/**
* 计算函数
*/
easingFn?: (t: number, b: number, c: number, d: number) => number;
/**
* 动画开始后传给父组件的回调
*/
started?: () => void;
/**
* 动画结束传递给父组件的回调
*/
ended?: () => void;
}
interface State {
start: number;
paused: boolean;
duration: number;
}
export default function Index({
end,
start = 0,
duration = 3000,
autoPlay = true,
decimals = 0,
decimal = '.',
separator = ',',
prefix = '',
suffix = '',
useEasing = true,
easingFn = (t, b, c, d) => (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b,
started = () => {},
ended = () => {},
}: Props) {
const isNumber = (val: string) => {
return !isNaN(parseFloat(val));
};
// 格式化数据,返回想要展示的数据格式
const formatNumber = (n: number) => {
let val = '';
if (n % 1 !== 0) val = n.toFixed(decimals);
const x = val.split('.');
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : '';
const rgx = /(\d+)(\d{3})/;
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + separator + '$2');
}
}
return prefix + x1 + x2 + suffix;
};
const [state, setState] = useState<State>({
start: 0,
paused: false,
duration,
});
const startTime = useRef(0);
const _timestamp = useRef(0);
const remaining = useRef(0);
const printVal = useRef(0);
const rAf = useRef(0);
const endRef = useRef(end);
const endedCallback = useRef(ended);
const [displayValue, setValue] = useState(formatNumber(start));
// 定义一个计算属性,当开始数字大于结束数字时返回true
const stopCount = useMemo(() => start > end, [start, end]);
const count = (timestamp: number) => {
if (!startTime.current) startTime.current = timestamp;
_timestamp.current = timestamp;
const progress = timestamp - startTime.current;
remaining.current = state.duration - progress;
// 是否使用速度变化曲线
if (useEasing) {
if (stopCount) {
printVal.current = state.start - easingFn(progress, 0, state.start - end, state.duration);
} else {
printVal.current = easingFn(progress, state.start, end - state.start, state.duration);
}
} else {
if (stopCount) {
printVal.current = state.start - (state.start - endRef.current) * (progress / state.duration);
} else {
printVal.current = state.start + (endRef.current - state.start) * (progress / state.duration);
}
}
if (stopCount) {
printVal.current = printVal.current < endRef.current ? endRef.current : printVal.current;
} else {
printVal.current = printVal.current > endRef.current ? endRef.current : printVal.current;
}
setValue(formatNumber(printVal.current));
if (progress < state.duration) {
rAf.current = requestAnimationFrame(count);
} else {
endedCallback.current?.();
}
};
const startCount = () => {
setState({ ...state, start, duration, paused: false });
rAf.current = requestAnimationFrame(count);
startTime.current = 0;
};
useEffect(() => {
if (!autoPlay) {
cancelAnimationFrame(rAf.current);
setState({ ...state, paused: true });
}
}, [autoPlay]);
useEffect(() => {
if (!state.paused) {
cancelAnimationFrame(rAf.current);
startCount();
}
}, [start]);
useEffect(() => {
if (autoPlay) {
startCount();
started?.();
}
return () => {
cancelAnimationFrame(rAf.current);
};
}, []);
return displayValue;
}