irpas技术客

React官网的井字棋游戏_努力学习代码的凇凇_react 游戏

大大的周 4989

React官网的井字棋游戏

这个是我在官网再次复习这个小游戏时梳理的一些思路,其中也包含了我在尝试时出的一些bug

文章目录 React官网的井字棋游戏1.整体分析项目2.为变量添加state并绑定点击事件3.轮流落子4.判断输赢5.时间旅行6.展示历史步骤7.总结

1.整体分析项目

先对整个项目进行分析,可以分割为哪些组件,组件之间应该如何联系

这个小游戏中一共分为三个组件

Square组件:用来渲染每一个小方格

Board组件:控制九个方格这一面板

Game组件:控制更宏观的东西

可以通过初步的分析设计出这三个组件,以及确定好组件之间的关系

可以先由此写出一个没有交互,没有变化的静态页面

2.为变量添加state并绑定点击事件

接下来为这个项目添加"活力"

1.Square组件需要实时打印’X’、'Y’或空白,是变化量,因此要设置为state可以修改

2.但是在这个游戏中,需要判断输赢,单独的九个Square无法关联,因此需要状态提升

将这个value设置值为Board的State,再通过参数Props传入Square,就能实现在Board层面,宏观控制九个值的关系,判断输赢

constructor(props){ super(props); this.state={ squares: Array(9).fill(null), } }

在Board组件中声明

3.根据用户的点击,要在棋盘上落子,因此要响应onClick事件

但是,onClick是直接与Square的botton相关联,而state在Board中

直接的onClick处理函数无法越级更改父级中的State

因此,onClick事件的处理一定要在Board组件中

所以,可以在Square中额外添加一个onClick属性,由父组件的props传入,这样在点击时就会调用Board里的处理函数

value的值也与onClick一样由Board传入

相当于这个点击事件的状态也随之提升了

Square组件也被称为受控组件,完全由Board进行控制

4.下面需要完善Board里的点击事件处理函数

handleClick(i){ const squares =this.state.squares.slice(); squares[i]='X'; this.setState({ squares: squares, }) }

需要注意的是,在这里,我们用新的一份数据来替换旧的数据,这样比直接修改数据有一些好处

如果直接进行修改的话,很难追踪到数据的改变,而用整体替换,相当于有很多的历史版本,对于数据维护,或者是撤销等复杂功能提供了更好的实现方法。

写完之后点击会在相应的方格出现’X’

但是我在这里犯了两个小错误:

在这里,应该传递的是数字这个索引,而不是直接传递值

如果传递数字的话,它就像一个Id,有很高的复用性,但是值得复用性就很低

因此要把8也改为this.renderSquare(8) (前几个我已经改回来了)

然后运行后点击还是没有反应,对比代码后发现,传入的onClick必须加括号

可能因为onClick的值是一段js代码,所以被理解为了函数

修改完之后就可以正常运行了

5.由于Square只有一个render方法,可以改写成函数组件,更方便一些

像这样:

function Square(props){ return( <button className="square" onClick={props.onClick()}> {props.value} </button> ); }

但是之后又又又报错了,,找了半天才发现改为函数组件后,要去掉后面的括号

这样才是对的

3.轮流落子

在这里需要在state中额外添加一个变量来储存,下一位应该是哪个玩家/改玩家能不能落子

这个地方要写圆括号,写大括号会报错

4.判断输赢

要写一个能判断输赢的全局函数

function calculateWinner(squares){ const lines=[ [0,1,2], [3,4,5], [6,7,8], [0,3,6], [1,4,7], [2,5,8], [0,4,8], [2,4,6], ]; for(let i=0;i<lines.length;i++){ const [a,b,c]=lines[i]; if(squares[a]&&squares[a]===squares[b]&&squares[a]===squares[c]){ return squares[a]; } } return null; }

要防止用户多次点击,或者在决出胜者之后,不能再落子

一个类组件一般包含几部分

第一部分一般写构造函数、事件处理函数等等

第二部分一般写一些逻辑代码

第三步部分return内的,代表想在页面上展示的

5.时间旅行

实现时间回溯的功能,与之前的squares.slice()处理有很大的关系

用这种切片的处理,让数据追踪变得容易,每一次都是静态不变的历史数据版本,只要再声明一个history将其储存下来即可

我们想用最外层的game组件实现这一部分,因此history应声明在history的state中,同时要将board组件里的squares状态再次梯提升,这样history就能直接与其操作了

注意大括号

用Array声明相当于执行了js代码,所以要套在大括号里

修改后的点击事件处理函数

6.展示历史步骤

在处理未知个数据时,可以用map方法,把它们一个个都渲染出来

写成move元素的形式,然后再在最下面调用即可

比直接写在下面要好得多

在构建动态列表时,要指定一个合适的key

系统会根据key查找上次与其相同的元素,进行比较,然后重新渲染

最后引入一个state参数,stepNumber,来记录版本号,这样处理会更方便一些

两个核心函数:

所有代码:

import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; function Square(props){ return( <button className="square" onClick={props.onClick}> {props.value} </button> ); } class Board extends React.Component { renderSquare(i) { return (<Square value={this.props.squares[i]} onClick={()=>this.props.onClick(i)} /> ) } render() { return ( <div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } } class Game extends React.Component { constructor(props){ super(props); this.state={ history: [ {squares: Array(9).fill(null)}, ], xIsNext: true, stepNumber: 0, } } handleClick(i){ const history=this.state.history.slice(0,this.state.stepNumber+1); const current=history[history.length-1]; const squares=current.squares.slice(); if(squares[i]||calculateWinner(squares)){ return; } squares[i]=this.state.xIsNext?'X':'O'; this.setState({ history: history.concat([{ squares: squares, }]), xIsNext: !this.state.xIsNext, stepNumber: history.length, }); } jumpTo(step){ this.setState({ stepNumber: step, xIsNext: (step%2)===0, }) } render() { const history=this.state.history; const current=history[this.state.stepNumber]; const winner=calculateWinner(current.squares); const moves=history.map((step,move)=>{ const desc=move?'Go to move #'+move:'Go to game start'; return( <li key={move}> <botton onClick={()=>this.jumpTo(move)}>{desc}</botton> </li> ) }) let status; if(winner){ status='Winner: '+ winner; }else{ status='Next Player: '+(this.state.xIsNext?'X':'O'); } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={(i)=>this.handleClick(i)} /> </div> <div className="game-info"> <div>{ status }</div> <ol>{moves}</ol> </div> </div> ); } } // ======================================== const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<Game />); function calculateWinner(squares){ const lines=[ [0,1,2], [3,4,5], [6,7,8], [0,3,6], [1,4,7], [2,5,8], [0,4,8], [2,4,6], ]; for(let i=0;i<lines.length;i++){ const [a,b,c]=lines[i]; if(squares[a]&&squares[a]===squares[b]&&squares[a]===squares[c]){ return squares[a]; } } return null; }

效果:

7.总结

在学习技术的时候,永远是要勤写,光停留在理论层面是学不会的,在自己敲代码的时候,能留意到更多的细节,也会引发更多的思考,同时也会加深对这个项目的理解,也能积累下来很多代码的经验

在写代码的过程中要仔细,一个小小的错误可能会为找bug带来巨大的苦恼,所以尽量不犯小错误,能减少bug的出现,保护头发

在IT行业,一方面技术更新迭代很快,另一方面确实感受到知识面很广,要学很多的东西,了解很多的东西,还是要多学多看多了解,慢慢积累

在前端项目中,尤其是大型项目,一般要使用React+Axios,

简单的项目一般使用jQuary+$.ajax就可以了

当然前端也有使用很广泛的Vue框架,这个等之后有空再学

复习到这里,基本上也算复习完了,在暑期实训的同时,要把axios学了,再学一学与后端对接的知识

加油!!


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #React #游戏 #React官网的井字棋小游戏