"Can't perform a React state update on an unmounted component" 后续连续 "TYPE ERORR: null is not an object"

"Can't perform a React state update on an unmounted component" Follow by continuous "TYPE ERORR: null is not an object"

我正在开发的应用是一款多游戏应用(小鸟游戏、鼹鼠游戏和扫雷游戏)。开始屏幕是菜单。它在这个屏幕上给我一个循环警告。当我点击开始地鼠游戏时,它会自然播放,但是当我按下后退按钮返回菜单时,我收到错误消息:“无法在未安装的组件上执行 React 状态更新" 错误后跟连续 "类型错误:null 不是对象"。

截图:

Cycle warning

Null is not an object

Node.js errors

地鼠游戏的App.js

import {
    View,
    StyleSheet,
    Image,
    SafeAreaView,
    Text,
    TouchableWithoutFeedback
} from 'react-native';
import Images from './assets/Images';
import Constants from './Constants';
import Mole from './Mole';
import GameOver from './GameOver';
import Clear from './Clear';
import Pause from './Pause';

const DEFAULT_TIME = 20;
const DEFAULT_STATE = {
    level: 1,
    score: 0,
    time: DEFAULT_TIME,
    cleared: false,
    paused: false,
    gameover: false,
    health: 100
}

export default class MoleGame extends Component {
    constructor(props) {
        super(props);
        this.moles = [];
        this.state = DEFAULT_STATE;
        this.molesPopping = 0;
        this.interval = null;
        this.timeInterval = null;

    }

    componentDidMount = () => {
        this.setupTicks(DEFAULT_STATE, this.pause);
    }


    setupTicks = () => {
        let speed = 750 - (this.state.level * 50);
        if (speed < 350) {
            speed = 350;
        }
        this.interval = setInterval(this.popRandomMole, speed);
        this.timeInterval = setInterval(this.timerTick, 1000);
    }

    reset = () => {
        this.molesPopping = 0;
        this.setState(DEFAULT_STATE, this.setupTicks)
    }

    pause = () => {
        if (this.interval) clearInterval(this.interval);
        if (this.timeInterval) clearInterval(this.timeInterval);
        this.setState({
            paused: true
        });
    }

    resume = () => {
        this.molesPopping = 0;
        this.setState({
            paused: false
        }, this.setupTicks);
    }

    nextLevel = () => {
        this.molesPopping = 0;

        this.setState({
            level: this.state.level + 1,
            cleared: false,
            gameover: false,
            time: DEFAULT_TIME
        }, this.setupTicks)
    }

    timerTick = () => {
        if (this.state.time === 0) {
            clearInterval(this.interval);
            clearInterval(this.timeInterval);
            this.setState({
                cleared: true
            })
        } else {
            this.setState({
                time: this.state.time - 1
            })
        }
    }

    gameOver = () => {
        clearInterval(this.interval);
        clearInterval(this.timerInterval);
        this.setState({
            gameover: true
        })

    }

    onDamage = () => {
        if (this.state.cleared || this.state.gameOver || this.state.paused) {
            return;
        }
        let targetHealth = this.state.health - 10 < 0 ? 0 : this.state.health - 20;

        this.setState({
            health: targetHealth
        });

        if (targetHealth <= 0) {
            this.gameOver();
        }
    }

    onHeal = () => {
        let targetHealth = this.state.health + 10 > 100 ? 100 : this.state.health + 10
        this.setState({
            health: targetHealth
        })
    }

    onScore = () => {
        this.setState({
            score: this.state.score + 1
        })
    }

    randomBetween = (min, max) => {
        return Math.floor(Math.random() * (max - min + 1) + min);
    }

    onFinishPopping = (index) => {
        this.molesPopping -= 1;
    }

    popRandomMole = () => {
        if (this.moles.length != 12) {
            return;
        }
        let randomIndex = this.randomBetween(0, 11);
        if (!this.moles[randomIndex].isPopping && this.molesPopping < 3) {
            this.molesPopping += 1;
            this.moles[randomIndex].pop();
        }
    }


    render() {
        let healthBarWidth = (Constants.MAX_WIDTH - Constants.XR * 100 - Constants.XR * 60 - Constants.XR * 6) * this.state.health / 100;
        return (
            <View style={styles.container}>
                <Image style={styles.backgroundImage} resizeMode="stretch" source={Images.background} />
                <View style={styles.topPanel}>
                    <SafeAreaView>
                        <View style={styles.statsContainer}>
                            <View style={styles.stats}>
                                <View style={styles.levelContainer}>
                                    <Text style={styles.levelTitle}>Level</Text>
                                    <Text style={styles.levelNumber}>{this.state.level}</Text>
                                </View>
                            </View>
                            <View style={styles.stats}>
                                <View style={styles.timeBar}>
                                    <Text style={styles.timeNumber}>{this.state.time}</Text>
                                </View>
                                <Image style={styles.timeIcon} resizeMode="stretch" source={Images.timeIcon} />
                            </View>
                            <View style={styles.stats}>
                                <View style={styles.scoreBar}>
                                    <Text style={styles.scoreNumber}>{this.state.score}</Text>
                                </View>
                                <Image style={styles.scoreIcon} resizeMode="stretch" source={Images.scoreIcon} />
                            </View>
                            <View style={styles.stats}>
                                <TouchableWithoutFeedback onPress={this.pause}>
                                    <View style={styles.pauseButton}>
                                        <Image style={styles.pauseButtonIcon} resizeMode="stretch" source={Images.pauseIcon} />
                                    </View>
                                </TouchableWithoutFeedback>
                            </View>
                        </View>

                        <View style={styles.healthBarContainer}>
                            <View style={styles.healthBar}>
                                <View style={[styles.healthBarInner, { width: healthBarWidth }]} />
                            </View>
                            <Image style={styles.healthIcon} resizeMode="stretch" source={Images.healthIcon} />
                        </View>
                    </SafeAreaView>
                </View>
                <View style={styles.playArea}>
                    {Array.apply(null, Array(4)).map((el, rowIdx) => {
                        return (
                            <View style={styles.playRow} key={rowIdx}>
                                {Array.apply(null, Array(3)).map((el, colIdx) => {
                                    let moleIdx = (rowIdx * 3) + colIdx;

                                    return (
                                        <View style={styles.playCell} key={colIdx}>
                                            <Mole
                                                index={moleIdx}
                                                onDamage={this.onDamage}
                                                onHeal={this.onHeal}
                                                onFinishPopping={this.onFinishPopping}
                                                onScore={this.onScore}
                                                ref={(ref) => { this.moles[moleIdx] = ref }}
                                            />
                                        </View>
                                    )
                                })}
                            </View>
                        )
                    })}
                </View>
                {this.state.cleared && <Clear onReset={this.reset} onNextLevel={this.nextLevel} level={this.state.level} score={this.state.score} />}
                {this.state.gameover && <GameOver onReset={this.reset} level={this.state.level} score={this.state.score} />}
                {this.state.paused && <Pause onReset={this.reset} onResume={this.resume} />}
            </View>
        )
    }
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        flexDirection: 'column'
    },
    backgroundImage: {
        width: Constants.MAX_WIDTH,
        height: Constants.MAX_HEIGHT,
        position: 'absolute'
    },
    topPanel: {
        position: 'absolute',
        top: 0,
        left: 0,
        right: 0,
        height: Constants.YR * 250,
        flexDirection: 'column'
    },
    statsContainer: {
        width: Constants.MAX_WIDTH,
        height: Constants.YR * 120,
        flexDirection: 'row'
    },
    stats: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center'
    },
    pauseButton: {
        width: Constants.YR * 50,
        height: Constants.YR * 50,
        backgroundColor: 'black',
        borderColor: 'white',
        borderWidth: 3,
        borderRadius: 10,
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center'
    },
    pauseButtonIcon: {
        width: Constants.YR * 25,
        height: Constants.YR * 25,
    },
    levelContainer: {
        width: Constants.YR * 80,
        height: Constants.YR * 80,
        backgroundColor: '#ff1a1a',
        borderColor: 'white',
        borderWidth: 3,
        borderRadius: 10,
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center'
    },
    levelTitle: {
        fontSize: 21,
        color: 'white',
        fontFamily: 'LilitaOne'
    },
    levelNumber: {
        fontSize: 17,
        color: 'white',
        fontFamily: 'LilitaOne'
    },
    scoreIcon: {
        position: 'absolute',
        left: 0,
        width: Constants.YR * 40,
        height: Constants.YR * 40,
    },
    scoreBar: {
        height: Constants.YR * 25,
        position: 'absolute',
        left: 20,
        right: 5,
        backgroundColor: 'white',
        borderRadius: 13,
        justifyContent: 'center',
        alignItems: 'center'
    },
    scoreNumber: {
        fontSize: 17,
        color: 'black',
        fontFamily: 'LilitaOne',
    },
    timeIcon: {
        position: 'absolute',
        left: 0,
        width: Constants.YR * 40,
        height: Constants.YR * 40,
    },
    timeBar: {
        height: Constants.YR * 25,
        position: 'absolute',
        left: 20,
        right: 5,
        backgroundColor: 'white',
        borderRadius: 13,
        justifyContent: 'center',
        alignItems: 'center'
    },
    timeNumber: {
        fontSize: 17,
        color: 'black',
        fontFamily: 'LilitaOne',
    },
    healthBarContainer: {
        height: Constants.YR * 40,
        width: Constants.MAX_WIDTH - Constants.XR * 120,
        marginLeft: Constants.XR * 60
    },
    healthIcon: {
        position: 'absolute',
        top: 0,
        left: 0,
        width: Constants.YR * 46,
        height: Constants.YR * 40,
    },
    healthBar: {
        height: Constants.YR * 20,
        width: Constants.MAX_WIDTH - Constants.XR * 100 - Constants.XR * 60,
        marginLeft: Constants.XR * 40,
        marginTop: Constants.YR * 10,
        backgroundColor: 'white',
        borderRadius: Constants.YR * 10
    },
    healthBarInner: {
        position: 'absolute',
        backgroundColor: '#ff1a1a',
        left: Constants.XR * 3,

        top: Constants.YR * 3,
        bottom: Constants.YR * 3,
        borderRadius: Constants.YR * 8
    },
    playArea: {
        width: Constants.MAX_WIDTH,
        marginTop: Constants.YR * 250,
        height: Constants.MAX_HEIGHT - Constants.YR * 250 - Constants.YR * 112,
        flexDirection: 'column',
    },
    playRow: {
        height: (Constants.MAX_HEIGHT - Constants.YR * 250 - Constants.YR * 112) / 4,
        width: Constants.MAX_WIDTH,
        flexDirection: 'row',
    },
    playCell: {
        width: Constants.MAX_WIDTH / 3,
        height: (Constants.MAX_HEIGHT - Constants.YR * 250 - Constants.YR * 112) / 4,
        alignItems: 'center'
    }
});

Mole.js

import { View, StyleSheet, Button, Image, TouchableWithoutFeedback } from 'react-native';
import Images from './assets/Images';
import SpriteSheet from 'rn-sprite-sheet';
//import Constants from './Constants';

export default class Mole extends Component {
    constructor (props) {
        super(props);
        this.mole = null;
        this.actionTimeout = null;
        this.isPopping = false;
        this.isWacked = false; 
        this.isHealing = false; 
        this.isAttacking = false; 
        this.isFeisty = false; 
    }

    pop =()=>{
        this.isWacked = false;
        this.isPopping = true;
        this.isAttacking = false; 

        this.isFeisty = Math.random() < 0.4;
        if(!this.isFeisty){
            this.isHealing = Math.random() < 0.12;
        }

        if(this.isHealing){
            this.mole.play({
                type: "heal",
                onFinish :  ()=>{
                    this.actionTimeout = setTimeout(()=>{
                        this.mole.play({
                            type : "hide",
                            fps: 24, 
                            onFinish: ()=>{
                                this.isPopping = false; 
                                this.props.onFinishPopping(this.props.index);
                            }
                        })
                    }, 1000); 
                }
            })
        }
        else{
            this.mole.play({
                type : "appear",
                fps: 24,
                onFinish: ()=>{
                    if (this.isFeisty){ 
                        this.actionTimeout = setTimeout(() => { 
                            this.isAttacking = true;    
                            this.props.onDamage();  
                            this.mole.play({    
                                type: "attack", 
                                fps: 12,    
                                onFinish: () => {   
                                    this.mole.play({    
                                        type: "hide",   
                                        fps: 24,    
                                        onFinish: () => {   
                                            this.isPopping = false; 
                                            this.props.onFinishPopping(this.props.index);   
                                        }   
                                    })  
                                }   
                            })  
                        }, 1000)
                    }
                    else{
                        this.actionTimeout = setTimeout(()=>{
                            this.mole.play({
                                type : "hide",
                                fps: 24, 
                                onFinish: ()=>{
                                    this.isPopping = false; 
                                    this.props.onFinishPopping(this.props.index);
                                }
                            })
                        }, 1000);
                    }
                    
                }
            })
        }
    }

    whack = ()=>{
        if(!this.isPopping || this.isWacked || this.isAttacking){
            return;
        }
        if (this.actionTimeout){
            clearTimeout(this.actionTimeout);
        }
        this.isWacked = true;
        
        this.props.onScore ();
        if( this.isHealing){
            this.props.onHeal();
        }
        this.mole.play({
            type: "dizzy",
            fps: 24,
            onFinish: () => {
                this.mole.play({
                    type: "faint",
                    fps: 24,
                    onFinish: () => {
                        this.isPopping = false;
                        this.props.onFinishPopping(this.props.index);
                    }
                })
            }
        })
    }


    render() {
        return (
            <View style= {styles.container}>
                <SpriteSheet  
                ref= {ref=> {this.mole = ref}}
                source = {Images.sprites}
                columns = {6}
                rows = {8}
                width = {100} 
                animations = {{
                    idle: [0],
                    appear: [1,2,3,4],
                    hide: [4,3,2,1,0],
                    dizzy : [36,37,38],
                    faint: [42,43,44,0],
                    attack: [11,12,13,14,15,16],
                    heal: [24,25,26,27,28,29,30,31,32,33]
                }} />
                <TouchableWithoutFeedback onPress= {this.whack} style= {{position: 'absolute', top: 0 , bottom:0, left:0, right:0 }}>
                    <View style= {{position: 'absolute', top: 0 , bottom:0, left:0, right:0 }} />
                </TouchableWithoutFeedback>  
            </View>
        )
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1
    }
})

index.js(导航)

import React from "react";
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import MoleGame from './Game1/App.js'
import BirdGame from './Game2/App.js'
import HomeScreen from './Game3/screens/HomeScreen.js'
import GameScreen from './Game3/screens/GameScreen.js'
import Home from './Home.js'


const Stack = createStackNavigator();

export default App = () => {
    return (
        <NavigationContainer>
            <Stack.Navigator screenOptions={{ headerShown: false, gestureEnabled: false, }}  >
                <Stack.Screen name="Home" component={Home} />
                <Stack.Screen name="MoleGame" component={MoleGame} />
                <Stack.Screen name="BirdGame" component={BirdGame} />
                <Stack.Screen name="MineSweeperHome" component={HomeScreen} />
                <Stack.Screen name="MineSweeperGame" component={GameScreen} />
            </Stack.Navigator>
        </NavigationContainer>
    );
};

Home.js

import { View, Text, TouchableOpacity, Dimensions, StyleSheet } from 'react-native';

const GameButton = ({ label, onPress, style }) => {
    return (
        <TouchableOpacity
            style={style}
            onPress={onPress}
        >
            <Text style={styles.text}>{label}</Text>
        </TouchableOpacity>
    );
};

export default Home = ({ navigation }) => {
    return (
        <View style={styles.container}>
            <View style={styles.welcome}>
                <Text style={styles.text}> Welcome! </Text>
            </View>
            <View style={styles.buttonContainer}>
                <GameButton
                    label={'Play Bird Game'}
                    onPress={() => navigation.navigate('BirdGame')}
                    style={styles.gameButton}
                />
                <GameButton
                    label={'Play Mole Game'}
                    onPress={() => navigation.navigate('MoleGame')}
                    style={{ ...styles.gameButton, backgroundColor: 'green' }}
                />
                <GameButton
                    label={'Play MineSweeper Game'}
                    onPress={() => navigation.navigate('MineSweeperHome')}
                    style={{ ...styles.gameButton, backgroundColor: 'grey' }}
                />
            </View>
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#FFCB43'
    },
    welcome: {
        alignItems: 'center',
        justifyContent: 'center',
        padding: 10,
        marginTop: 80,
    },
    buttonContainer: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'space-evenly'
    },
    gameButton: {
        height: 70,
        width: Dimensions.get("screen").width / 1.4,
        backgroundColor: 'red',
        borderColor: 'red',
        alignItems: 'center',
        justifyContent: 'center',
        borderRadius: 20,
        borderWidth: 2,
        borderColor: 'black',
    },
    text: {
        fontSize: 22,
    }
})

一些上下文和注意事项

更新 :

添加后

componentWillUnmount(){
        clearInterval(this.interval);
        clearInterval(this.timerInterval);
    }

到App.js。我不再连续出现“TypeError: null is not an object (evaluating '_this.moles[randomIndex].isPopping')”。不幸的是,我仍然得到 TypeError: null is not an object (evaluating '_this.moles.play) 错误。当我返回主菜单时会发生这种情况。我尝试在 mole.js 上添加 clearTimeout(this.actionTimeout),但没有任何效果。

截图:

TypeError: null is not an object (evaluating '_this.moles.play)

The setInterval() function is commonly used to set a delay for functions that are executed again and again, such as animations. You can cancel the interval using clearInterval().

当您从 MoleGame 导航到 Home 时,MoleGame 路由会从导航堆栈中弹出,并且其组件会被卸载。但是 setupTicks 方法的间隔仍在执行,并尝试在 MoleGame 组件上设置状态并访问 this (这两者都不可能)。

componentWillUnmount 上尝试 clearInterval 以停止在 setupTicks 方法中设置的间隔。

// App.js
export default class MoleGame extends Component {
    constructor(props) {
        super(props);
        this.interval = null;
        this.timeInterval = null;
        ...
    }

    componentDidMount = () => {
        this.setupTicks(DEFAULT_STATE, this.pause);
    }

    componentWillUnmount() {
        clearInterval(this.interval);
        clearInterval(this.timeInterval);
    }
}

类似地,在 Mole.js 中,您必须通过使用 clearTimeout()

来处理任何 setTimeout() 的相同情况

Further reading: Plugging memory leaks in your app

  1. 通过添加:
componentWillUnmount(){
        clearInterval(this.interval);
        clearInterval(this.timerInterval);
    }

它修复了连续的 TypeError: null is not an object (evaluating '_this.moles[randomIndex].isPopping')

  1. 要删除 "TypeError: null is not an object (evaluating '_this.moles.play)" 错误,我必须将 this.mole.play 放入 Mole.js 内的变量中,因为当我返回菜单时 this 不再引用 this.mole.play。仍然有内存泄漏警告,因为当我转到菜单时它正在完成动画,但没有更多严重错误。