React官网的井字棋游戏
这个是我在官网再次复习这个小游戏时梳理的一些思路,其中也包含了我在尝试时出的一些bug
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学了,再学一学与后端对接的知识
加油!!