You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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;

优化原理

  1. Store仅存关键状态:移除now字段,不再每秒更新全局状态,避免触发所有订阅组件的重渲染。
  2. 本地计时器更新:仅在LiveTimer组件内部维护计时器更新显示,其他组件(按钮、 laps列表)仅在start、laps、isRunning这些核心状态变化时才重渲染。
  3. 精准状态订阅:使用Zustand的选择器仅订阅组件所需的状态,避免订阅整个store带来的不必要重渲染。

内容的提问来源于stack exchange,提问作者Elvys Pons

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 12:14:52