优化 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