秒表与 useState Hook 完美配合,但我需要使用全局状态管理来存储时间。我在我的应用程序中使用 Zustand,到目前为止,存储和更新状态没有任何问题,但现在我一直在寻找一种有效的解决方案来存储和更新秒表状态,而无需每秒重新渲染组件,但我还没有成功。
根据我所做的一些研究,我尝试实现 useRef、Transient Updates 和 subscribe 方法,但没有任何效果。该组件每秒都会重新渲染,从而影响最佳计时器性能。我希望在 Zustand 中存储和更新状态,而无需每秒重新渲染组件。谁能给我任何建议或提示来解决这个问题?预先感谢。
这是 GameClock1.tsx 组件:
import React, {useEffect, useRef} from 'react';
import {
StyleSheet,
Text,
View,
ScrollView,
TouchableOpacity,
} from 'react-native';
import moment from 'moment';
import useClockStore from '../../Store/GameClockStore';
interface TimerProps {
interval: number;
style: any;
}
function Timer({interval, style}: TimerProps) {
const pad = (n: number) => (n < 10 ? '0' + n : n);
const duration = moment.duration(interval);
const centiseconds = Math.floor(duration.milliseconds() / 10);
return (
<View style={styles.timerContainer}>
<Text style={style}>{pad(duration.minutes())}:</Text>
<Text style={style}>{pad(duration.seconds())},</Text>
<Text style={style}>{pad(centiseconds)}</Text>
</View>
);
}
interface RoundButtonProps {
title: string;
color: string;
background: string;
onPress?: () => void;
disabled?: boolean;
}
function RoundButton({
title,
color,
background,
onPress,
disabled,
}: RoundButtonProps) {
return (
<TouchableOpacity
onPress={() => !disabled && onPress && onPress()}
style={[styles.button, {backgroundColor: background}]}
activeOpacity={disabled ? 1.0 : 0.7}>
<View style={styles.buttonBorder}>
<Text style={[styles.buttonTitle, {color}]}>{title}</Text>
</View>
</TouchableOpacity>
);
}
interface LapProps {
number: number;
interval: number;
fastest: boolean;
slowest: boolean;
}
function Lap({number, interval, fastest, slowest}: LapProps) {
const lapStyle = [
styles.lapText,
fastest && styles.fastest,
slowest && styles.slowest,
];
return (
<View style={styles.lap}>
<Text style={lapStyle}>Lap {number}</Text>
<Timer style={[lapStyle, styles.lapTimer]} interval={interval} />
</View>
);
}
interface LapsTableProps {
laps: number[];
timer: number;
}
function LapsTable({laps, timer}: LapsTableProps) {
const storeClock = useClockStore();
const finishedLaps = storeClock.state.laps?.slice(1);
let min = Number.MAX_SAFE_INTEGER;
let max = Number.MIN_SAFE_INTEGER;
if (finishedLaps?.length >= 2) {
finishedLaps.forEach(lap => {
if (lap < min) {
min = lap;
}
if (lap > max) {
max = lap;
}
});
}
return (
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
{storeClock.state.laps?.map((lap, index) => (
<Lap
number={storeClock.state.laps.length - index}
key={storeClock.state.laps.length - index}
interval={index === 0 ? timer + lap : lap}
fastest={lap === min}
slowest={lap === max}
/>
))}
</ScrollView>
);
}
interface ButtonsRowProps {
children: React.ReactNode;
}
function ButtonsRow({children}: ButtonsRowProps) {
return <View style={styles.buttonsRow}>{children}</View>;
}
function GameClock1() {
const storeClock = useClockStore();
const intervalIdRef = useRef<ReturnType<typeof setInterval> | number | null>(
null,
);
useEffect(() => {
return () => {
clearInterval(intervalIdRef.current as number);
};
}, []);
const start = () => {
const now = new Date().getTime();
storeClock.setState({
start: now,
now,
laps: [0],
});
intervalIdRef.current = setInterval(() => {
storeClock.setState({
now: new Date().getTime(),
});
}, 100);
};
const lap = () => {
const timestamp = new Date().getTime();
const [firstLap, ...other] = storeClock.state.laps;
storeClock.setState({
laps: [
0,
firstLap + storeClock.state.now - storeClock.state.start,
...other,
],
start: timestamp,
now: timestamp,
});
};
const stop = () => {
clearInterval(intervalIdRef.current as number);
const [firstLap, ...other] = storeClock.state.laps;
storeClock.setState({
laps: [
firstLap + storeClock.state.now - storeClock.state.start,
...other,
],
start: 0,
now: 0,
});
};
const reset = () => {
storeClock.setState({
laps: [],
start: 0,
now: 0,
});
};
const resume = () => {
const now = new Date().getTime();
const [...other] = storeClock.state.laps;
storeClock.setState({
start: now,
now,
laps: [...other],
});
intervalIdRef.current = setInterval(() => {
storeClock.setState({
now: new Date().getTime(),
});
}, 100);
};
const timer = storeClock.state.now - storeClock.state.start;
return (
<View style={styles.container}>
<Timer
interval={
storeClock.state.laps?.reduce((total, curr) => total + curr, 0) +
timer
}
style={styles.timer}
/>
{storeClock.state.laps?.length === 0 && (
<ButtonsRow>
<RoundButton
title="Lap"
color="#8B8B90"
background="#151515"
disabled
/>
<RoundButton
title="Start"
color="#50D167"
background="#1B361F"
onPress={start}
/>
</ButtonsRow>
)}
{storeClock.state.start > 0 && (
<ButtonsRow>
<RoundButton
title="Lap"
color="#FFFFFF"
background="#3D3D3D"
onPress={lap}
/>
<RoundButton
title="Stop"
color="#E33935"
background="#3C1715"
onPress={stop}
/>
</ButtonsRow>
)}
{storeClock.state.laps?.length > 0 && storeClock.state.start === 0 && (
<ButtonsRow>
<RoundButton
title="Reset"
color="#FFFFFF"
background="#3D3D3D"
onPress={reset}
/>
<RoundButton
title="Start"
color="#50D167"
background="#1B361F"
onPress={resume}
/>
</ButtonsRow>
)}
<LapsTable laps={storeClock.state.laps} timer={timer} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0D0D0D',
alignItems: 'center',
paddingTop: 130,
paddingHorizontal: 20,
},
timer: {
color: '#FFFFFF',
fontSize: 76,
fontWeight: '200',
width: 110,
},
button: {
width: 80,
height: 80,
borderRadius: 40,
justifyContent: 'center',
alignItems: 'center',
},
buttonTitle: {
fontSize: 18,
},
buttonBorder: {
width: 76,
height: 76,
borderRadius: 38,
borderWidth: 1,
justifyContent: 'center',
alignItems: 'center',
},
buttonsRow: {
flexDirection: 'row',
alignSelf: 'stretch',
justifyContent: 'space-between',
marginTop: 80,
marginBottom: 30,
},
lapText: {
color: '#FFFFFF',
fontSize: 18,
},
lapTimer: {
width: 30,
},
lap: {
flexDirection: 'row',
justifyContent: 'space-between',
borderColor: '#151515',
borderTopWidth: 1,
paddingVertical: 10,
},
scrollView: {
alignSelf: 'stretch',
},
fastest: {
color: '#4BC05F',
},
slowest: {
color: '#CC3531',
},
timerContainer: {
flexDirection: 'row',
},
});
export default GameClock1;`
这是我的 Zustand 商店 (GameClockStore.tsx):
import {create} from 'zustand';
export interface ClockState {
state: {
start: number;
now: number;
laps: number[];
};
}
export interface Actions {
setState: (newState: Partial<ClockState['state']>) => void;
}
const useClockStore = create<ClockState & Actions>()(set => ({
state: {
start: 0,
now: 0,
laps: [],
},
setState: newState => set(state => ({state: {...state.state, ...newState}})),
}))
export default useClockStore;
要使用 Zustand 存储新的 Date().getTime() 方法并更新计时器而不每秒重新渲染组件,您可以利用 Zustand 中间件进行瞬时更新。这是实现这一目标的简化方法:
修改您的 Zustand 商店:
从 'zustand' 导入 { create };
interface ClockState {
start: number;
now: number;
laps: number[];
setStart: (start: number) => void;
setNow: (now: number) => void;
addLap: (lap: number) => void;
reset: () => void;
}
const useClockStore = create<ClockState>((set) => ({
start: 0,
now: 0,
laps: [],
setStart: (start) => set({ start }),
setNow: (now) => set({ now }),
addLap: (lap) => set((state) => ({ laps: [lap, ...state.laps] })),
reset: () => set({ start: 0, now: 0, laps: [] }),
}));
export default useClockStore;
更新组件中的定时器逻辑
使用 useRef 和 useEffect 来管理计时器逻辑,而不会导致每次更新时重新渲染。
import React, { useEffect, useRef } from 'react';
import { StyleSheet, Text, View, ScrollView, TouchableOpacity } from 'react-native';
import moment from 'moment';
import useClockStore from '../../Store/GameClockStore';
interface TimerProps {
interval: number;
style: any;
}
function Timer({ interval, style }: TimerProps) {
const pad = (n: number) => (n < 10 ? '0' + n : n);
const duration = moment.duration(interval);
const centiseconds = Math.floor(duration.milliseconds() / 10);
return (
<View style={styles.timerContainer}>
<Text style={style}>{pad(duration.minutes())}:</Text>
<Text style={style}>{pad(duration.seconds())},</Text>
<Text style={style}>{pad(centiseconds)}</Text>
</View>
);
}
interface RoundButtonProps {
title: string;
color: string;
background: string;
onPress?: () => void;
disabled?: boolean;
}
function RoundButton({ title, color, background, onPress, disabled }: RoundButtonProps) {
return (
<TouchableOpacity
onPress={() => !disabled && onPress && onPress()}
style={[styles.button, { backgroundColor: background }]}
activeOpacity={disabled ? 1.0 : 0.7}
>
<View style={styles.buttonBorder}>
<Text style={[styles.buttonTitle, { color }]}>{title}</Text>
</View>
</TouchableOpacity>
);
}
interface LapProps {
number: number;
interval: number;
fastest: boolean;
slowest: boolean;
}
function Lap({ number, interval, fastest, slowest }: LapProps) {
const lapStyle = [
styles.lapText,
fastest && styles.fastest,
slowest && styles.slowest,
];
return (
<View style={styles.lap}>
<Text style={lapStyle}>Lap {number}</Text>
<Timer style={[lapStyle, styles.lapTimer]} interval={interval} />
</View>
);
}
interface LapsTableProps {
laps: number[];
timer: number;
}
function LapsTable({ laps, timer }: LapsTableProps) {
const storeClock = useClockStore();
const finishedLaps = storeClock.laps.slice(1);
let min = Number.MAX_SAFE_INTEGER;
let max = Number.MIN_SAFE_INTEGER;
if (finishedLaps.length >= 2) {
finishedLaps.forEach(lap => {
if (lap < min) {
min = lap;
}
if (lap > max) {
max = lap;
}
});
}
return (
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
{storeClock.laps.map((lap, index) => (
<Lap
number={storeClock.laps.length - index}
key={storeClock.laps.length - index}
interval={index === 0 ? timer + lap : lap}
fastest={lap === min}
slowest={lap === max}
/>
))}
</ScrollView>
);
}
interface ButtonsRowProps {
children: React.ReactNode;
}
function ButtonsRow({ children }: ButtonsRowProps) {
return <View style={styles.buttonsRow}>{children}</View>;
}
function GameClock1() {
const { start, now, laps, setStart, setNow, addLap, reset } = useClockStore();
const intervalIdRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
return () => {
if (intervalIdRef.current) {
clearInterval(intervalIdRef.current);
}
};
}, []);
const startTimer = () => {
const nowTime = new Date().getTime();
setStart(nowTime);
setNow(nowTime);
intervalIdRef.current = setInterval(() => {
setNow(new Date().getTime());
}, 100);
};
const lap = () => {
const timestamp = new Date().getTime();
addLap(now - start);
setStart(timestamp);
setNow(timestamp);
};
const stop = () => {
if (intervalIdRef.current) {
clearInterval(intervalIdRef.current);
}
addLap(now - start);
setStart(0);
setNow(0);
};
const resetTimer = () => {
reset();
};
const resume = () => {
const nowTime = new Date().getTime();
setStart(nowTime);
setNow(nowTime);
intervalIdRef.current = setInterval(() => {
setNow(new Date().getTime());
}, 100);
};
const timer = now - start;
return (
<View style={styles.container}>
<Timer
interval={laps.reduce((total, curr) => total + curr, 0) + timer}
style={styles.timer}
/>
{laps.length === 0 && (
<ButtonsRow>
<RoundButton
title="Lap"
color="#8B8B90"
background="#151515"
disabled
/>
<RoundButton
title="Start"
color="#50D167"
background="#1B361F"
onPress={startTimer}
/>
</ButtonsRow>
)}
{start > 0 && (
<ButtonsRow>
<RoundButton
title="Lap"
color="#FFFFFF"
background="#3D3D3D"
onPress={lap}
/>
<RoundButton
title="Stop"
color="#E33935"
background="#3C1715"
onPress={stop}
/>
</ButtonsRow>
)}
{laps.length > 0 && start === 0 && (
<ButtonsRow>
<RoundButton
title="Reset"
color="#FFFFFF"
background="#3D3D3D"
onPress={resetTimer}
/>
<RoundButton
title="Start"
color="#50D167"
background="#1B361F"
onPress={resume}
/>
</ButtonsRow>
)}
<LapsTable laps={laps} timer={timer} />
</View>
);
}