尝试用 React 制作 2048 游戏。状态中的数据在不应该发生变化时发生了变化

Posted

技术标签:

【中文标题】尝试用 React 制作 2048 游戏。状态中的数据在不应该发生变化时发生了变化【英文标题】:Try to make 2048 game in React. Data in the state is mutated when it should not have been 【发布时间】:2018-09-09 19:05:24 【问题描述】:

这是我的代码仓库https://github.com/540376482yzb/2048game_react

这是我的现场演示https://objective-fermat-0343a3.netlify.com/

游戏的当前状态只会左右移动。

但是,当没有移动时,它会在按下左箭头键时生成新数字。已确定的问题是 this.state.gridData 在我运行 this.diffGrid(grid) 之前被翻转,因此它将始终返回 true 并添加新数字。

这个问题的嫌疑人是:

    // issue ====>  flipGrid function is mutating this.state.gridData
flipGrid(grid) 
    return grid.map(row => row.reverse())

或者

    //slide left
            if (e.keyCode === 37) 
                //issue ===> state is flipped on left arrow key pressed
                copyGrid = this.flipGrid(copyGrid).map(row => this.slideAndCombine(row))
                copyGrid = this.flipGrid(copyGrid)
            

谁能告诉我我在哪里做错了导致状态突变?

import React from 'react'
import './App.css'
import Grid from './Grid'
class App extends React.Component 
	constructor(props) 
		super(props)
		this.state = 
			gridData: [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
		
	
	initGame() 
		let grid = [...this.state.gridData]
		grid = this.addNumber(grid)
		grid = this.addNumber(grid)
		this.setState(
			gridData: grid
		)
	

	addNumber(grid) 
		const availableSpot = []
		grid.map((rowData, x) =>
			rowData.map((data, y) => 
				if (!data) availableSpot.push( x, y )
			)
		)
		const randomSpot = availableSpot[Math.floor(Math.random() * availableSpot.length)]
		grid[randomSpot.x][randomSpot.y] = Math.random() < 0.2 ? 4 : 2
		return grid
	
	slide(row) 
		const newRow = row.filter(data => data)
		const zerosArr = Array(4 - newRow.length).fill(0)
		return [...zerosArr, ...newRow]
	

	combine(row) 
		let a, b
		for (let i = 3; i > 0; i--) 
			a = row[i]
			b = row[i - 1]
			if (a === b) 
				row[i] = a + b
				row[i - 1] = 0
			
		
		return row
	

	slideAndCombine(row) 
		row = this.slide(row)
		row = this.combine(row)
		return row
	

	diffGrid(grid) 
		let isDiff = false
		for (let i = 0; i < grid.length; i++) 
			for (let j = 0; j < grid.length; j++) 
				if (grid[i][j] != this.state.gridData[i][j]) 
					isDiff = true
				
			
		
		return isDiff
	

	// issue ====>  flipGrid function is mutating this.state.gridData
	flipGrid(grid) 
		return grid.map(row => row.reverse())
	

	componentDidMount() 
		this.initGame()
		let copyGrid = [...this.state.gridData]
		window.addEventListener('keyup', e => 
			if (e.keyCode === 37 || 38 || 39 || 40) 
				//slide right
				if (e.keyCode === 39) 
					copyGrid = copyGrid.map(row => this.slideAndCombine(row))
				
				//slide left
				if (e.keyCode === 37) 
					//issue ===> state is flipped on left arrow key pressed
					copyGrid = this.flipGrid(copyGrid).map(row => this.slideAndCombine(row))
					copyGrid = this.flipGrid(copyGrid)
				

				// Line 89 issue==>>>>>> gridData in the state
				console.table(this.state.gridData)

				// diffGrid compares copyGrid with this.state.gridData
				if (this.diffGrid(copyGrid)) 
					copyGrid = this.addNumber(copyGrid)
					//deepCopy of gridData
					console.table(copyGrid)

					this.setState(
						gridData: copyGrid
					)
				
			
		)
	

	render() 
		// Line 103 ===>>>> gridData in the state
		console.table(this.state.gridData)
		return (
			<div className="App">
				<main className="centerGrid" id="game">
					<Grid gridData=this.state.gridData />
				</main>
			</div>
		)
	


export default App

【问题讨论】:

【参考方案1】:

row.reverse() 将返回反转的数组,但它也会改变行数组。查看 reverse 的 MDN 文档。因此,由于 map 在迭代元素之前不会复制元素,因此您的 flipGrid 函数将改变传递给它的网格。

为确保您不会改变原始网格,您可以执行以下操作:

flipGrid(grid) 
    const gridCopy = grid.slice()
    return gridCopy.map(row => row.reverse())

或者更简洁一点:

flipGrid(grid) 
    return grid.slice().map(row => row.reverse())

或者如果您想使用扩展运算符:

flipGrid(grid) 
    return [...grid].map(row => row.reverse())

至于为什么this.state.gridData会发生变异:数组传播其实是一个浅拷贝,所以gridCopy仍然引用this.state.gridData的行数组,正因为如此你变异的时候仍然在变异this.state.gridData与翻转网格。如果您完全停止变异,这无关紧要,但最好在事件处理程序的开头而不是在 componentDidMount 中进行深度复制,这样一开始就不会发生这种情况。

您可以使用 lodash 的 cloneDeep 函数或在 gridCopy 上映射并浅拷贝它们(使用扩展运算符或切片)

window.addEventListener('keyup', e => 
    if (e.keyCode === 37 || 38 || 39 || 40) 
        let gridCopy = this.state.gridData.map((row) => row.slice())
        etc...
    

【讨论】:

我传递给flipGrid(copyGrid)的参数是this.state.gridData的深拷贝数组,所以它应该不会影响原始数据,对吗? 我使用扩展运算符 [...] 而不是 .slice() 来解决问题 @ZhouBingYang 嘿,我更新了我的答案,如果有什么没有意义的,请再次评论:) 谢谢。是的,我没有意识到在嵌套数组上使用 [...] 正在做浅拷贝。我今天学到了很多。谢谢先生。 @ZhouBingYang 没问题 :)

以上是关于尝试用 React 制作 2048 游戏。状态中的数据在不应该发生变化时发生了变化的主要内容,如果未能解决你的问题,请参考以下文章

如何通过 swiftUI 制作 2048 游戏 [关闭]

制作2048游戏

Python 中的 2048 游戏

Qt 制作2048小游戏

2048小游戏竟然还有3D版?使用MATLAB制作一款3D版2048小游戏

2048小游戏竟然还有3D版?使用MATLAB制作一款3D版2048小游戏