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官网的井字棋小游戏