如何用Zustand存储Date.getTime()并实现无每秒重渲染的计时器更新
解决Zustand秒表组件频繁重渲染的方案
核心问题分析
你的代码中,每秒通过setInterval更新Zustand里的now状态,而GameClock1组件直接订阅了整个store,导致每次now更新都会触发整个组件(包括按钮、 laps列表)重渲染,这是性能损耗的根源。
优化方案步骤
1. 重构Zustand Store:只存关键状态,把计时器逻辑移到store内部
修改GameClockStore.tsx,移除now字段,添加计时器控制方法,用ref存储intervalID(避免组件持有计时器实例):
import { create } from 'zustand'; import { useRef } from 'react'; export interface ClockState { start: number; laps: number[]; isRunning: boolean; } export interface Actions { startTimer: () => void; stopTimer: () => void; lap: () => void; reset: () => void; resume: () => void; } const useClockStore = create<ClockState & Actions>()((set, get) => { const intervalIdRef = useRef<ReturnType<typeof setInterval> | null>(null); const clearActiveInterval = () => { if (intervalIdRef.current) { clearInterval(intervalIdRef.current); intervalIdRef.current = null; } }; return { start: 0, laps: [], isRunning: false, startTimer: () => { const now = Date.now(); clearActiveInterval(); set({ start: now, laps: [0], isRunning: true, }); }, stopTimer: () => { clearActiveInterval(); const { start, laps } = get(); const totalElapsed = Date.now() - start; const [firstLap, ...other] = laps; set({ laps: [firstLap + totalElapsed, ...other], start: 0, isRunning: false, }); }, lap: () => { const { start, laps } = get(); const now = Date.now(); const totalElapsed = now - start; const [firstLap, ...other] = laps; set({ laps: [0, firstLap + totalElapsed, ...other], start: now, }); }, reset: () => { clearActiveInterval(); set({ start: 0, laps: [], isRunning: false, }); }, resume: () => { const now = Date.now(); const { laps } = get(); clearActiveInterval(); set({ start: now, isRunning: true, }); }, }; }); export default useClockStore;
2. 拆分组件:让仅计时器部分实时更新
修改GameClock1.tsx,将计时器显示拆成独立组件,该组件仅本地计算时间差,不依赖store的实时更新:
import React, { useEffect, useState, useRef } from 'react'; // 保留原有其他导入 // 独立的实时计时器组件,仅负责更新显示 function LiveTimer({ baseTime, elapsedAddition, style }: { baseTime: number; elapsedAddition: number; style: any; }) { const [elapsed, setElapsed] = useState(0); const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null); useEffect(() => { if (baseTime === 0) { setElapsed(elapsedAddition); intervalRef.current && clearInterval(intervalRef.current); return; } const updateElapsed = () => { setElapsed(Date.now() - baseTime + elapsedAddition); }; updateElapsed(); intervalRef.current = setInterval(updateElapsed, 100); return () => intervalRef.current && clearInterval(intervalRef.current); }, [baseTime, elapsedAddition]); return <Timer interval={elapsed} style={style} />; } // 保留原有RoundButton、Lap、ButtonsRow组件不变 // 优化LapsTable:仅接收props,不直接订阅store interface LapsTableProps { laps: number[]; timer: number; } function LapsTable({ laps, timer }: LapsTableProps) { const finishedLaps = 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}> {laps.map((lap, index) => ( <Lap number={laps.length - index} key={laps.length - index} interval={index === 0 ? timer + lap : lap} fastest={lap === min} slowest={lap === max} /> ))} </ScrollView> ); } function GameClock1() { // 仅订阅组件需要的状态和方法,避免订阅整个store const { start, laps, isRunning, startTimer, stopTimer, lap, reset, resume } = useClockStore(state => ({ start: state.start, laps: state.laps, isRunning: state.isRunning, startTimer: state.startTimer, stopTimer: state.stopTimer, lap: state.lap, reset: state.reset, resume: state.resume, })); // 计算已完成 laps 的总时间 const totalLapsTime = laps.reduce((sum, lap) => sum + lap, 0); return ( <View style={styles.container}> {/* 仅该组件每秒更新,其他组件不受影响 */} <LiveTimer baseTime={start} elapsedAddition={totalLapsTime} 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> )} {isRunning && ( <ButtonsRow> <RoundButton title="Lap" color="#FFFFFF" background="#3D3D3D" onPress={lap} /> <RoundButton title="Stop" color="#E33935" background="#3C1715" onPress={stopTimer} /> </ButtonsRow> )} {laps.length > 0 && !isRunning && ( <ButtonsRow> <RoundButton title="Reset" color="#FFFFFF" background="#3D3D3D" onPress={reset} /> <RoundButton title="Start" color="#50D167" background="#1B361F" onPress={resume} /> </ButtonsRow> )} <LapsTable laps={laps} timer={start ? Date.now() - start : 0} /> </View> ); } // 保留原有styles不变 export default GameClock1;
优化原理
- Store仅存关键状态:移除
now字段,不再每秒更新全局状态,避免触发所有订阅组件的重渲染。 - 本地计时器更新:仅在
LiveTimer组件内部维护计时器更新显示,其他组件(按钮、 laps列表)仅在start、laps、isRunning这些核心状态变化时才重渲染。 - 精准状态订阅:使用Zustand的选择器仅订阅组件所需的状态,避免订阅整个store带来的不必要重渲染。
内容的提问来源于stack exchange,提问作者Elvys Pons
相关产品推荐
相关产品推荐

