康心伴Logo
康心伴WellAlly
前端开发

优化 React 状态以处理可穿戴设备数据流

5 分钟阅读

优化 React 状态以处理可穿戴设备数据流

概述

可穿戴设备持续产生大量实时数据——心率、步数、睡眠、血氧等。高效处理这些数据流是健康应用性能的关键。本文将介绍一系列优化 React 状态管理的策略。

数据流特征分析

可穿戴设备数据特点

  • 高频更新:心率每秒更新、步数实时累积
  • 数据量大:全天数据可达数万条记录
  • 实时性要求:延迟影响用户体验
  • 需聚合处理:原始数据需转换为有意义指标

数据分类策略

code
// lib/types/wearable.ts

export enum DataFrequency {
  INSTANT = 'instant',     // 毫秒级(心率、加速度)
  SECONDLY = 'secondly',   // 秒级(步数、卡路里)
  MINUTELY = 'minutely',   // 分钟级(活动状态)
  HOURLY = 'hourly',       // 小时级(睡眠阶段)
  DAILY = 'daily'          // 日级(总步数、睡眠时长)
}

export interface WearableDataPoint {
  timestamp: number;
  type: DataFrequency;
  deviceId: string;
}

// 高频数据(毫秒级)
export interface InstantData extends WearableDataPoint {
  type: DataFrequency.INSTANT;
  metric: 'heartRate' | 'acceleration' | 'spo2';
  value: number;
}

// 实时数据(秒级)
export interface SecondlyData extends WearableDataPoint {
  type: DataFrequency.SECONDLY;
  metric: 'steps' | 'calories';
  value: number;
}

// 聚合数据(日级)
export interface DailyData extends WearableDataPoint {
  type: DataFrequency.DAILY;
  metric: 'totalSteps' | 'totalSleep' | 'activeMinutes';
  value: number;
}
Code collapsed

状态管理架构

分层状态设计

code
// stores/wearableStore.ts

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

interface WearableState {
  // 第一层:实时数据(最新值)
  realtime: {
    heartRate: number | null;
    steps: number;
    calories: number;
    spo2: number | null;
    lastUpdate: number;
  };

  // 第二层:短期缓存(最近5分钟)
  shortTermCache: {
    data: Array<{
      timestamp: number;
      heartRate?: number;
      steps?: number;
    }>;
    maxSize: number;
  };

  // 第三层:会话数据(当前活动)
  session: {
    isActive: boolean;
    startTime: number | null;
    data: Map<string, number[]>;
  };

  // 第四层:持久化数据(已完成记录)
  historical: {
    byDate: Map<string, DailySummary>;
  };
}

// 使用 Immer 简化嵌套更新
export const useWearableStore = create<WearableState>()(
  immer((set, get) => ({
    realtime: {
      heartRate: null,
      steps: 0,
      calories: 0,
      spo2: null,
      lastUpdate: 0
    },
    shortTermCache: {
      data: [],
      maxSize: 300 // 5分钟 * 60秒
    },
    session: {
      isActive: false,
      startTime: null,
      data: new Map()
    },
    historical: {
      byDate: new Map()
    },

    // 更新实时数据(最频繁操作)
    updateRealtime: (metric: keyof WearableState['realtime'], value: number) =>
      set((state) => {
        state.realtime[metric] = value as never;
        state.realtime.lastUpdate = Date.now();
      }),

    // 添加到缓存(防抖处理)
    addToCache: (data: { timestamp: number; heartRate?: number; steps?: number }) =>
      set((state) => {
        state.shortTermCache.data.push(data);

        // 保持缓存大小
        if (state.shortTermCache.data.length > state.shortTermCache.maxSize) {
          state.shortTermCache.data.shift();
        }
      }),

    // 会话管理
    startSession: () =>
      set((state) => {
        state.session.isActive = true;
        state.session.startTime = Date.now();
        state.session.data.clear();
      }),

    endSession: () =>
      set((state) => {
        state.session.isActive = false;
        // 将会话数据移至历史记录
        const sessionData = state.session.data;
        const dateKey = new Date().toISOString().split('T')[0];

        if (!state.historical.byDate.has(dateKey)) {
          state.historical.byDate.set(dateKey, {
            date: dateKey,
            heartRateData: [],
            stepsData: []
          });
        }

        const dailyData = state.historical.byDate.get(dateKey)!;
        dailyData.heartRateData.push(...(sessionData.get('heartRate') || []));
        dailyData.stepsData.push(...(sessionData.get('steps') || []));

        state.session.data.clear();
      })
  }))
);
Code collapsed

数据流优化策略

1. 数据批处理

code
// hooks/useWearableDataStream.ts

import { useCallback, useRef, useEffect } from 'react';

const BATCH_SIZE = 50; // 每50个数据点处理一次
const BATCH_INTERVAL = 1000; // 或每1秒处理一次

export const useWearableDataStream = (deviceId: string) => {
  const batchRef = useRef<WearableDataPoint[]>([]);
  const timeoutRef = useRef<NodeJS.Timeout>();

  const processBatch = useCallback((data: WearableDataPoint[]) => {
    const { updateRealtime, addToCache } = useWearableStore.getState();

    // 按数据类型分组
    const byType = data.reduce((acc, point) => {
      if (!acc[point.metric]) acc[point.metric] = [];
      acc[point.metric].push(point);
      return acc;
    }, {} as Record<string, WearableDataPoint[]>);

    // 处理心率数据
    if (byType.heartRate) {
      const latestHeartRate = byType.heartRate[byType.heartRate.length - 1];
      updateRealtime('heartRate', latestHeartRate.value);
    }

    // 更新缓存
    data.forEach(point => {
      addToCache({
        timestamp: point.timestamp,
        heartRate: point.metric === 'heartRate' ? point.value : undefined,
        steps: point.metric === 'steps' ? point.value : undefined
      });
    });
  }, []);

  const handleDataPoint = useCallback((point: WearableDataPoint) => {
    batchRef.current.push(point);

    // 达到批处理大小
    if (batchRef.current.length >= BATCH_SIZE) {
      processBatch([...batchRef.current]);
      batchRef.current = [];

      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    }
  }, [processBatch]);

  // 定时处理未完成的批次
  useEffect(() => {
    const interval = setInterval(() => {
      if (batchRef.current.length > 0) {
        processBatch([...batchRef.current]);
        batchRef.current = [];
      }
    }, BATCH_INTERVAL);

    return () => clearInterval(interval);
  }, [processBatch]);

  return { handleDataPoint };
};
Code collapsed

2. 数据抽样

code
// lib/data-sampling.ts

/**
 * 对高频数据进行智能抽样
 */
export class DataSampler {
  private windowSize: number;
  private buffer: WearableDataPoint[] = [];

  constructor(windowSize: number = 1000) {
    this.windowSize = windowSize;
  }

  /**
   * 添加数据点并返回是否需要处理
   */
  add(point: WearableDataPoint): boolean {
    this.buffer.push(point);

    // 检查时间窗口
    const now = point.timestamp;
    const windowStart = now - this.windowSize;

    // 移除窗口外的数据
    this.buffer = this.buffer.filter(p => p.timestamp >= windowStart);

    // 抽样策略:重要变化 + 定期采样
    const shouldProcess = this.shouldProcessData(point);
    return shouldProcess;
  }

  private shouldProcessData(point: WearableDataPoint): boolean {
    if (this.buffer.length < 2) return true;

    const previous = this.buffer[this.buffer.length - 2];

    // 心率:变化超过5 bpm
    if (point.metric === 'heartRate') {
      return Math.abs(point.value - previous.value) >= 5;
    }

    // 步数:每10个点采样一次
    if (point.metric === 'steps') {
      return this.buffer.length % 10 === 0;
    }

    // SpO2:变化超过2%
    if (point.metric === 'spo2') {
      return Math.abs(point.value - previous.value) >= 2;
    }

    return false;
  }

  /**
   * 获取聚合数据
   */
  getAggregated(): WearableDataPoint[] {
    // 按指标分组
    const byMetric = new Map<string, WearableDataPoint[]>();

    for (const point of this.buffer) {
      if (!byMetric.has(point.metric)) {
        byMetric.set(point.metric, []);
      }
      byMetric.get(point.metric)!.push(point);
    }

    // 计算每个指标的统计值
    const aggregated: WearableDataPoint[] = [];

    for (const [metric, points] of byMetric) {
      const avg = points.reduce((sum, p) => sum + p.value, 0) / points.length;
      const max = Math.max(...points.map(p => p.value));
      const min = Math.min(...points.map(p => p.value));

      aggregated.push({
        timestamp: Date.now(),
        deviceId: points[0].deviceId,
        type: DataFrequency.MINUTELY,
        metric: metric as any,
        value: avg
      });
    }

    return aggregated;
  }
}
Code collapsed

3. React 性能优化

code
// components/WearableDataChart.tsx

import React, { memo, useMemo, useCallback } from 'react';
import { Line } from 'react-chartjs-2';
import { useWearableStore } from '@/stores/wearableStore';
import { shallow } from 'zustand/shallow';

// 使用 memo 防止不必要的重渲染
export const WearableDataChart = memo(({ metric }: { metric: 'heartRate' | 'steps' }) => {
  // 使用选择器并应用浅比较
  const data = useWearableStore(
    (state) => state.shortTermCache.data,
    shallow
  );

  // 记忆化图表数据
  const chartData = useMemo(() => {
    const now = Date.now();
    const fiveMinutesAgo = now - 5 * 60 * 1000;

    const filteredData = data.filter(
      point => point.timestamp >= fiveMinutesAgo && point[metric] !== undefined
    );

    return {
      labels: filteredData.map(point =>
        new Date(point.timestamp).toLocaleTimeString()
      ),
      datasets: [{
        label: metric,
        data: filteredData.map(point => point[metric]!),
        borderColor: metric === 'heartRate' ? '#ef4444' : '#3b82f6',
        tension: 0.4
      }]
    };
  }, [data, metric]);

  // 记忆化回调
  const handlePointClick = useCallback((point: any) => {
    console.log('Clicked:', point);
  }, []);

  return (
    <div className="w-full h-64">
      <Line data={chartData} onClick={handlePointClick} />
    </div>
  );
});

WearableDataChart.displayName = 'WearableDataChart';
Code collapsed

4. Web Worker 处理

code
// lib/wearableWorker.ts

// 主线程
export class WearableDataProcessor {
  private worker: Worker | null = null;
  private callbacks: Map<string, (data: any) => void> = new Map();

  constructor() {
    if (typeof Worker !== 'undefined') {
      this.worker = new Worker(
        new URL('./wearableDataWorker.ts', import.meta.url),
        { type: 'module' }
      );

      this.worker.onmessage = (e) => {
        const { id, result } = e.data;
        const callback = this.callbacks.get(id);
        if (callback) {
          callback(result);
          this.callbacks.delete(id);
        }
      };
    }
  }

  processHeartRateData(data: number[]): Promise<HeartRateAnalysis> {
    return new Promise((resolve) => {
      const id = crypto.randomUUID();

      this.callbacks.set(id, resolve);

      this.worker?.postMessage({
        id,
        type: 'analyze-heart-rate',
        data
      });
    });
  }

  destroy() {
    this.worker?.terminate();
  }
}

// Web Worker (wearableDataWorker.ts)
self.onmessage = (e) => {
  const { id, type, data } = e.data;

  switch (type) {
    case 'analyze-heart-rate':
      const analysis = analyzeHeartRate(data);
      self.postMessage({ id, result: analysis });
      break;
  }
};

function analyzeHeartRate(data: number[]): HeartRateAnalysis {
  const avg = data.reduce((a, b) => a + b, 0) / data.length;
  const max = Math.max(...data);
  const min = Math.min(...data);
  const variance = data.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / data.length;
  const stdDev = Math.sqrt(variance);

  // 检测异常值
  const threshold = avg + 2 * stdDev;
  const anomalies = data.filter(val => val > threshold);

  return {
    average: avg,
    max,
    min,
    standardDeviation: stdDev,
    anomalies: anomalies.length,
    anomalyPercentage: (anomalies.length / data.length) * 100
  };
}
Code collapsed

状态持久化策略

code
// lib/wearablePersistence.ts

import { openDB } from 'idb';

export class WearablePersistence {
  private db: IDBDatabase | null = null;

  async init() {
    this.db = await openDB('wearable-data', 1, {
      upgrade(db) {
        // 实时数据存储(最近24小时)
        db.createObjectStore('realtime', { keyPath: 'timestamp' });

        // 压缩数据存储(历史记录)
        db.createObjectStore('historical', { keyPath: 'date' });

        // 会话数据
        db.createObjectStore('sessions', { keyPath: 'id', autoIncrement: true });
      }
    });
  }

  async saveRealtimeData(data: WearableDataPoint[]) {
    if (!this.db) await this.init();

    const tx = this.db!.transaction('realtime', 'readwrite');
    const store = tx.objectStore('realtime');

    // 只保留最近24小时数据
    const dayAgo = Date.now() - 24 * 60 * 60 * 1000;
    await store.delete(IDBKeyRange.upperBound(dayAgo));

    // 批量插入
    for (const point of data) {
      await store.put(point);
    }
  }

  async getHistoricalData(startDate: Date, endDate: Date): Promise<WearableDataPoint[]> {
    if (!this.db) await this.init();

    const tx = this.db!.transaction('historical', 'readonly');
    const store = tx.objectStore('historical');

    const results: WearableDataPoint[] = [];

    for await (const cursor of store.iterate(
      IDBKeyRange.bound(
        startDate.toISOString().split('T')[0],
        endDate.toISOString().split('T')[0]
      )
    )) {
      results.push(...cursor.value.data);
    }

    return results;
  }
}
Code collapsed

完整组件示例

code
// components/WearableDashboard.tsx

import React, { useEffect, useState } from 'react';
import { useWearableDataStream } from '@/hooks/useWearableDataStream';
import { WearableDataChart } from './WearableDataChart';
import { DataSampler } from '@/lib/data-sampling';
import { WearablePersistence } from '@/lib/wearablePersistence';

export const WearableDashboard: React.FC = () => {
  const [isConnected, setIsConnected] = useState(false);
  const samplerRef = useRef<DataSampler>(new DataSampler());
  const persistenceRef = useRef<WearablePersistence>(new WearablePersistence());

  const realtimeData = useWearableStore((state) => state.realtime);
  const { handleDataPoint } = useWearableDataStream('device-123');

  useEffect(() => {
    // 初始化持久化
    persistenceRef.current.init();

    // 模拟设备连接
    const connectToDevice = async () => {
      try {
        // 实际应用中这里会连接真实设备
        setIsConnected(true);

        // 模拟数据流
        const interval = setInterval(() => {
          const dataPoint: WearableDataPoint = {
            timestamp: Date.now(),
            deviceId: 'device-123',
            type: DataFrequency.INSTANT,
            metric: 'heartRate',
            value: 60 + Math.random() * 40
          };

          // 使用抽样器
          if (samplerRef.current.add(dataPoint)) {
            handleDataPoint(dataPoint);
          }
        }, 1000);

        return () => clearInterval(interval);
      } catch (error) {
        console.error('设备连接失败:', error);
        setIsConnected(false);
      }
    };

    connectToDevice();
  }, [handleDataPoint]);

  return (
    <div className="p-6 space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-2xl font-bold">可穿戴数据仪表板</h1>
        <div className="flex items-center gap-2">
          <div className={`w-3 h-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-gray-400'}`} />
          <span>{isConnected ? '已连接' : '未连接'}</span>
        </div>
      </div>

      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
        <MetricCard
          label="心率"
          value={realtimeData.heartRate}
          unit="bpm"
          icon={<HeartIcon />}
        />
        <MetricCard
          label="步数"
          value={realtimeData.steps}
          unit="步"
          icon={<FootprintsIcon />}
        />
        <MetricCard
          label="卡路里"
          value={realtimeData.calories}
          unit="kcal"
          icon={<FlameIcon />}
        />
        <MetricCard
          label="血氧"
          value={realtimeData.spo2}
          unit="%"
          icon={<DropletIcon />}
        />
      </div>

      <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
        <Card>
          <CardHeader>
            <CardTitle>心率趋势</CardTitle>
          </CardHeader>
          <CardContent>
            <WearableDataChart metric="heartRate" />
          </CardContent>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle>步数趋势</CardTitle>
          </CardHeader>
          <CardContent>
            <WearableDataChart metric="steps" />
          </CardContent>
        </Card>
      </div>
    </div>
  );
};
Code collapsed

参考资料

#

文章标签

react
状态优化
可穿戴设备
实时数据
性能优化

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅