上一期我们利用create-react-app搭建了好了一个react项目,这期我们通过跟随React官方教程--编写一个“井字棋"小游戏,来熟悉react的基本用法。
首先来看下“井字棋”的最终实现效果:
从演示中我们可以看到,这个游戏大致有以下功能:
1. 切换玩家;
2. 判定胜负;
3. 高亮获胜棋子;
4. 按步骤悔棋;
制作棋盘
首先我们需要制作一个棋盘。
在项目中新建一个Board.js文件:
import React from "react";
import "./style.css";
function Board() {
return (
<div className="board">
<h1>井字棋游戏--React</h1>
<button className="btn"></button>
<button className="btn"></button>
<button className="btn"></button>
<button className="btn"></button>
<button className="btn"></button>
<button className="btn"></button>
<button className="btn"></button>
<button className="btn"></button>
<button className="btn"></button>
</div>
);
}
export default Board;
Board.js
.btn {
width: 100px;
height: 100px;
border: 1px solid;
vertical-align: middle;
font-size: 30px;
font-weight: 600;
}
.board {
width: 300px;
margin-right: 20px;
}
css
import React from 'react';
import Board from './components/Board';
function App() {
return (
<div className="App">
<Board />
</div>
);
}
export default App;
app.js引入
不同于Vue的模板语法,react的这种写法叫做jsx语法,可以在js中直接编写dom标签。在渲染时,这种写法会被编译成 React.createElement('div',{className: 'board'})。
现在Board.js只有一个function Board,这种组件被称之为函数组件。
这种写法明显冗余,我们希望通过循环来生成9个方格,在Vue中可以使用v-for,那么在react中应该怎么做呢?
import React from "react";
import "./style.css";
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(""),
};
}
render() {
return (
<div className="board">
<h1>井字棋游戏--React</h1>
{this.state.squares.map((el, index) => {
return <button className="btn" key={index}></button>;
})}
</div>
);
}
}
export default Board;
首先我们将function Board 改成class Board 将其变为class组件。在constructor中定义一个state数据,其key为squares,是一个长度为9的值为空的数组。然后用map方法循环数组,输出dom节点。
接着我们将button标签拆分成单独的组件Square.js,以便学习父子组件的传值方式。
import React from "react";
import "./style.css";
function Square(props) {
return (
<button className="btn">
</button>
);
}
export default Square;
Square.js
import React from "react";
import Square from "./Square"
import "./style.css";
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(""),
};
}
render() {
return (
<div className="board">
<h1>井字棋游戏--React</h1>
{this.state.squares.map((el, index) => {
return <Square key={index}></Square>;
})}
</div>
);
}
}
export default Board;
Board.js
组件传值与事件交互
首先需要将玩家信息“X”or“O”传递给子组件。父组件通过绑定一个字段,子组件通过props接收。
import React from "react";
import Square from "./Square"
import "./style.css";
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(""),
player: "X"
};
}
render() {
return (
<div className="board">
<h1>井字棋游戏--React</h1>
{this.state.squares.map((el, index) => {
return <Square key={index} player={this.state.player}></Square>;
})}
</div>
);
}
}
export default Board;
import React from "react";
import "./style.css";
function Square(props) {
console.log(props);
return (
<button className="btn">
{props.player}
</button>
);
}
export default Square;
子组件拿到数据了,但是我们需要的是,每次点击时显示棋子并切换玩家。
思路是这样的:子组件在初始化时,拿空数据,每次点击时,修改当前节点的数据,并切换下一个玩家。
定义一个切换玩家的函数:changePlayer
changePlayer(index) {
let player = this.state.player === "X" ? "O" : "X";
let squares = [...this.state.squares];
squares[index] = player;
this.setState({
player,
squares
})
}
在react中是不允许直接修改state中的值的,必须通过setState来实现赋值,这个函数是异步的。
在react中,当数据改变时会触发render函数重新执行,生成虚拟dom,然后对比虚拟dom是否有改变,若有则重新渲染dom节点。
react组件生命周期
然后在子组件中通过属性传递事件函数
render() {
return (
<div className="board">
<h1>井字棋游戏--React</h1>
{this.state.squares.map((el, index) => {
return <Square key={index} player={el} changePlayer={() => {this.changePlayer(index)}}></Square>;
})}
</div>
);
}
}
向事件处理程序传递参数,需要注意this指向的问题,可以使用箭头函数或bind(this)来保持this的指向:
我个人更倾向于第一种。
接着在子组件中绑定click事件:
import React from "react";
import "./style.css";
function Square(props) {
return (
<button className="btn" onClick={props.changePlayer}>
{props.player}
</button>
);
}
export default Square;
这样基本的切换玩家功能就实现了。
判定胜负
“井字棋”的规则很简单,就是横、竖、对角线三子成棋,即为胜利。
我们先添加一些提示语:Next player:X/O
在玩家胜利时将其切换为:Winner is :X/O
定义winner与winnerArr(成棋的棋子的index数组)
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(""),
player: "X",
winner: "",
winnerArr: [],
};
}
通过条件判定,切换title
render() {
let { player, squares, winner } = this.state;
let title = "";
if (!winner) {
title = <p>Next player:{player}</p>;
} else {
title = <p>Winner is:{winner}</p>;
}
return (
<div className="board">
<h1>井字棋游戏--React</h1>
{title}
{squares.map((el, index) => {
return (
<Square
key={index}
player={el}
changePlayer={() => {
this.changePlayer(index);
}}
></Square>
);
})}
</div>
);
}
判定获胜者函数:该函数将所有获胜可能的index值组合穷举出来,再从棋盘上取index组合中的三个值,三值相等即为胜者,游戏结束。
// 判断获胜者
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: squares[a],
winnerArr: lines[i],
};
}
}
return null;
}
该函数在每次下棋动作都应被调用:
changePlayer(index) {
if (this.state.winner) {
return;
}
let player = this.state.player === "X" ? "O" : "X";
let squares = [...this.state.squares];
squares[index] = player;
this.setState({
player,
squares,
});
let winner = this.calculateWinner(squares);
if (winner) {
this.setState({
winner: winner.squares,
winnerArr: winner.winnerArr,
});
}
}
至此,判定胜者功能完成。
高亮获胜棋子
这个功能涉及动态添加className。
首先定义一个动态返回className的函数:
getClassName(index) {
let { winner, winnerArr } = this.state;
if (winner) {
for (let i = 0; i < 3; i++) {
if (winnerArr[i] === index) {
return "winner-square";
}
}
return "";
} else {
return "";
}
}
在组件中绑定:
return (
<div className="board">
<h1>井字棋游戏--React</h1>
{title}
{squares.map((el, index) => {
return (
<Square
key={index}
player={el}
changePlayer={() => {
this.changePlayer(index);
}}
winnerClass={this.getClassName(index)}
></Square>
);
})}
</div>
);
子组件内:
function Square(props) {
return (
<button className={`btn ${props.winnerClass}`} onClick={props.changePlayer}>
{props.player}
</button>
);
}
.winner-square {
background-color: aquamarine;
}
注意动态绑定className的语法:**{btn ${props.winnerClass}
} **是es6的模板字符串语法。当然还有其他表示方式,但是这种方式较为简洁,个人更推荐。
按步骤悔棋
其实这个功能只要将每一步的数据保存在一个数组当中,点击悔棋列表,将数据切换回去即可。
定义history保存初始状态值:
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(""),
player: "X",
winner: "",
history: [
{
squares: Array(9).fill(""),
player: "X",
},
],
step: 1,
winnerArr: [],
};
}
在切换玩家函数中保存每一步的数据:
// 切换玩家
changePlayer(i) {
if (this.state.winner) {
return;
}
let squares = [...this.state.squares];
let history = this.state.history.slice(0, this.state.step);
if (squares[i]) {
return;
}
let player = this.state.player === "X" ? "O" : "X";
squares[i] = this.state.player;
history.push({
squares,
player,
});
// setState方法是异步执行的
this.setState({
player,
squares,
history,
step: history.length,
});
let winner = this.calculateWinner(squares);
if (winner) {
this.setState({
winner: winner.squares,
winnerArr: winner.winnerArr
});
}
}
返回到某一步:
backTo(i) {
this.setState((state) => {
return {
winner: "",
squares: state.history[i].squares,
player: state.history[i].player,
step: i + 1,
};
});
}
添加悔棋dom节点,并添加触发事件:
// 每次数据更新都会触发执行
render() {
let { player, squares, winner, history } = this.state;
let title = "";
if (!winner) {
title = <p>Next player:{player}</p>;
} else {
title = <p>Winner is:{winner}</p>;
}
return (
<Fragment>
<h1>井字棋游戏--React</h1>
{title}
<div className="flex">
<div className="board">
{squares.map((el, index) => {
return (
<Square
changePlayer={() => this.changePlayer(index)}
key={index}
player={el}
index={index}
winnerClass={this.getClassName(index)}
/>
);
})}
</div>
<div className="back_step">
<p>悔棋</p>
{history.map((el, i) => {
return (
<button
key={i}
onClick={() => {
this.backTo(i);
}}
>
{i === 0 ? "Back to game start" : "Back to No:" + i + " step"}
</button>
);
})}
</div>
</div>
</Fragment>
);
}
至此功能就全部实现了!
需要获取源码的小伙伴,请进入公众号回复“react” 即可获取GitHub地址。
小伙伴们加油!我们下期再见!
全栈攻城狮进阶
关注微信公众号,第一时间获取好文章!
所有人都祝你快乐,我只愿你遍历山河,觉得人间值得。