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

Expo Go中AsyncStorage存储计时器值后无法正确读取求助

问题分析与修复方案

你的秒表应用在重启后无法正确恢复计时数据,核心问题出在异步存储的处理逻辑和计时器初始化逻辑上,以下是具体问题和修复步骤:


关键问题点

  1. saveAsync 未等待 AsyncStorage.setItem 完成,可能导致数据未成功写入就退出应用
  2. 计时器间隔逻辑错误:每次触发间隔时重复累加 retrievedTime,而非仅用它初始化一次计时
  3. 恢复数据时错误地将 retrievedTime 加到当前 time 上,而非直接替换为保存的值
  4. 未恢复 isRunning 状态,导致应用重启后无法自动继续计时
  5. displayTime 函数传入了未使用的参数,依赖组件状态却多此一举

修复步骤

1. 修复异步存储的 await 逻辑

AsyncStorage.setItem 是异步操作,必须等待它完成才能确保数据写入:

const saveAsync = async (key, value) => {
    await AsyncStorage.setItem(key, JSON.stringify(value)); // 添加 await
    console.log(key+" value saved: ", value);
};

2. 简化计时器间隔逻辑

移除间隔内的 retrievedTime 判断,因为我们会在初始化时直接将 time 设置为保存的值:

useEffect(() => {
    if (isRunning) {
        console.log("it is running");
        intervalRef.current = setInterval(() => {
            // 仅正常累加计时
            setTime(previousTime => previousTime + 100);    
        }, 100);
    } else {
        clearInterval(intervalRef.current);
        console.log("it stopped");
    }
    
    return () => clearInterval(intervalRef.current);
}, [isRunning]);

3. 正确恢复计时数据

将 time 直接设置为保存的 retrievedTime,而非累加:

useEffect(() => {
    setTime(retrievedTime);
}, [retrievedTime]);

4. 恢复运行状态

在初始化时同时读取 isRunning 状态,确保重启后能继续计时:

const retrieveAsync = async (key) => {
    try {
        const jsonValue = await AsyncStorage.getItem(key);
        if (jsonValue !== null) {
            const parsedValue = JSON.parse(jsonValue);
            console.log("parsedValue: ", parsedValue);
            
            if(key === 'time') {
                setRetrievedTime(parsedValue);
            } else if(key === 'isRunning') { // 新增处理 isRunning
                setIsRunning(parsedValue);
            }
        }
    } catch (e) {
        console.log('error: ', e);
    }
};

// 初始化时同时读取两个状态
useEffect(() => {
    retrieveAsync('time');
    retrieveAsync('isRunning');
}, []);

5. 修复 displayTime 函数

移除未使用的参数,直接使用组件状态:

const displayTime = () => {
    const milliseconds = parseInt((time%1000)/10);
    const centiseconds = Math.floor(milliseconds/10);
    const seconds = parseInt((time/1000)%60);
    const minutes = parseInt((time/(1000*60))%60)
    const hours = parseInt((time/(1000*60*60))%24);

    return {
        hours: pad(hours),
        minutesSeconds: pad(minutes % 60) + ':' + pad(seconds % 60),
        centiseconds: centiseconds,
    };
};

// JSX 中调用时无需传参
<Text style={[styles.hoursTimerOverlay, styles.textFont]}>{displayTime().hours}</Text>
<Text style={[styles.timerOverlay, styles.textFont]}>{displayTime().minutesSeconds}</Text>
<Text style={[styles.centisecondsTimerOverlay, styles.textFont]}>{displayTime().centiseconds}</Text>

完整修复后的代码

import React, { useState, useRef, useEffect, useCallback } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Image, Modal, Platform } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

export default function StopwatchScreen() {
    const [menuVisible, setMenuVisible] = useState(false);
    const [backgroundColor, setBackgroundColor] = useState('#11167F');
    const [headerBackgroundColor, setHeaderBackgroundColor] = useState('#0a1055');
    const [isRunning, setIsRunning] = useState(false);
    const [time, setTime] = useState(0);
    const [retrievedTime, setRetrievedTime] = useState(0);
    const intervalRef = useRef(null);

    const handleLapButtonPress = useCallback(() => {
        if(isRunning) {
            
        } else {
            setTime(0);
            setRetrievedTime(0);
            saveAsync('time', 0);
            console.log("cleared time: ", time);
            console.log("cleared retrieved time: ", retrievedTime);
        }
    }, [isRunning]);

    const handleStartButtonPress = async () => {
        setIsRunning(!isRunning);
        saveAsync('isRunning', !isRunning);
        saveAsync('time', time);
    };

    useEffect(() => {
        if (isRunning) {
            console.log("it is running");
            intervalRef.current = setInterval(() => {
                setTime(previousTime => previousTime + 100);    
            }, 100);
        } else {
            clearInterval(intervalRef.current);
            console.log("it stopped");
        }
        
        return () => clearInterval(intervalRef.current);
    }, [isRunning]);

    useEffect(() => {
        saveAsync('time', time);
    }, [time]);

    const toggleMenu = () => {
        setMenuVisible(!menuVisible);
    }

    const displayTime = () => {
        const milliseconds = parseInt((time%1000)/10);
        const centiseconds = Math.floor(milliseconds/10);
        const seconds = parseInt((time/1000)%60);
        const minutes = parseInt((time/(1000*60))%60)
        const hours = parseInt((time/(1000*60*60))%24);

        return {
            hours: pad(hours),
            minutesSeconds: pad(minutes % 60) + ':' + pad(seconds % 60),
            centiseconds: centiseconds,
        };
    };
    
    const pad = (number) => {
        return number < 10 ? '0' + number : number;
    };

    const retrieveAsync = async (key) => {
        try {
            const jsonValue = await AsyncStorage.getItem(key);
            if (jsonValue !== null) {
                const parsedValue = JSON.parse(jsonValue);
                console.log("parsedValue: ", parsedValue);
                
                if(key === 'time') {
                    setRetrievedTime(parsedValue);
                } else if(key === 'isRunning') {
                    setIsRunning(parsedValue);
                }
            }
        } catch (e) {
            console.log('error: ', e);
        }
    };

    const saveAsync = async (key, value) => {
        await AsyncStorage.setItem(key, JSON.stringify(value));
        console.log(key+" value saved: ", value);
    };

    useEffect(() => {
        setTime(retrievedTime);
    }, [retrievedTime]);

    useEffect(() => {
        retrieveAsync('time');
        retrieveAsync('isRunning');
    }, []);
    
    return (
        <View style={[styles.container, Platform.OS === 'android' && styles.androidPadding]}>
            <View style={[styles.header, {backgroundColor: headerBackgroundColor}]}>
                <Text style={[styles.title, styles.textFont]}>Futuristic Stopwatch</Text>
                <TouchableOpacity onPress={toggleMenu}>
                    <Image source={require('../assets/images/three_dots.png')} style={styles.dotsIcon} />
                </TouchableOpacity>
            </View>

            <Modal
                transparent={false}
                animationType="slide"
                visible={menuVisible}
                onRequestClose={() => setMenuVisible(false)}
            >
                <View style={styles.modalContainer}>
                    Modal
                </View>
            </Modal>

            <View style={[styles.contentContainer, {backgroundColor: backgroundColor}]}>
                <View style={styles.rowHud}>
                    <Text style={[styles.hoursTimerOverlay, styles.textFont]}>{displayTime().hours}</Text>
                    <Text style={[styles.timerOverlay, styles.textFont]}>{displayTime().minutesSeconds}</Text>
                    <Text style={[styles.centisecondsTimerOverlay, styles.textFont]}>{displayTime().centiseconds}</Text>
                </View>
                <View style={styles.controls}>
                    <TouchableOpacity onPress={handleStartButtonPress} style={[styles.controlButtonBorder, { backgroundColor: isRunning ? "#340e0d" : "#0a2a12" }]}>
                        <View style={styles.controlButton}>
                            <Text style={{ color: isRunning ? "#ea4c49" : "#37d05c" }}>{isRunning ? 'Stop' : 'Start'}</Text>
                        </View>
                    </TouchableOpacity>
                    <TouchableOpacity onPress={handleLapButtonPress} style={[styles.controlButtonBorder, { backgroundColor: isRunning ? "#333333" : "#1c1c1e" }]}>
                        <View style={styles.controlButton}>
                            <Text style={{ color: isRunning ? "#fff" : "#9d9ca2" }}>{isRunning ? 'Lap' : 'Reset'}</Text>
                        </View>
                    </TouchableOpacity>
                </View>
            </View>
        </View>
    );
}

const styles = StyleSheet.create({
    container: {
      flex: 1,
      backgroundColor: '#fff',
      padding: 0,
    },
    androidPadding: {
        paddingTop: Platform.OS === 'android' ? 25 : 0,
    },
    header: {
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: 0,
        paddingTop: 18,
        paddingBottom: 15,
        paddingLeft: 10,
        paddingRight: 10,
    },
    title: {
        fontSize: 18,
        color: 'white',
    },
    dotsIcon: {
        width: 20,
        height: 20,
        resizeMode: 'contain',
    },
    modalContainer: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: 'rgba(0, 0, 0, 0.5)',
    },
    textFont: {
        fontFamily: 'Orbitron Black',
    },
    contentContainer: {
        flex: 1,
    },
    rowHud: {
        flex: 0.6,
        flexDirection: 'row',
        marginBottom: 0,
        marginTop: 10
    },
    timerOverlay: {
        position: 'absolute',
        top: '50%',
        left: '50%',
        transform: [{ translateX: -90 }, { translateY: -30 }],
        fontSize: 50,
        color: 'white',
    },
    hoursTimerOverlay: {
        position: 'absolute',
        top: '35%',
        left: '50%',
        transform: [{ translateX: -30 }, { translateY: -30 }],
        fontSize: 35,
        color: 'white',
    },
    centisecondsTimerOverlay: {
        position: 'absolute',
        top: '71%',
        left: '50%',
        transform: [{ translateX: -15 }, { translateY: -35 }],
        fontSize: 35,
        color: 'white',
    },
    controls: {
        flexDirection: 'row',
        justifyContent: 'space-between',
        paddingLeft: 20,
        paddingRight: 20,
    },
    controlButtonBorder: {
        justifyContent: "center",
        alignItems: "center",
        width: 70,
        height: 70,
        borderRadius: 70,
    },
    controlButton: {
        justifyContent: "center",
        alignItems: "center",
        width: 65,
        height: 65,
        borderRadius: 65,
        borderColor: "#000",
        borderWidth: 1
    },
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 05:17:04