react官方文档提供了一个井字棋游戏教程,文档末尾提供了6条改进建议,本篇文章记录我对这些改进的实现。
1.在游戏历史记录列表显示每一步棋的坐标
在Game组件的history变量中维护一个名为position的对象,记录到达该棋盘状态的落棋位置坐标。
constructor(props){
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
position: {row: -1, col: -1},
}],
xIsNext: true,
stepNumber: 0,
}
}
修改handleClick()函数,每下一步,加入落棋位置position
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
let cur = history[history.length-1];
if(cur.squares[i]!=null || calculateWinner(cur.squares)!=null){
return;
}
const squares = cur.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
//获取该索引对应的坐标
let position = getPosition(i);
this.setState({
history: history.concat({squares,position}),
xIsNext: !this.state.xIsNext,
stepNumber: this.state.stepNumber+1,
});
}
getPosition函数的作用是根据索引index获取其在棋盘上的行和列
function getPosition(index){
let row = Math.trunc(index/3);
let col = index%3;
return {row,col};
}
js知识回顾 - Number类型的舍入
Math.trunc(IE 浏览器不支持这个方法)移除小数点后的所有内容而没有舍入:3.1 变成 3,-1.1 变成 -1。Math.floor向下舍入:3.1 变成 3,-1.1 变成 -2。Math.ceil向上舍入:3.1 变成 4,-1.1 变成 -1。Math.round向最近的整数舍入:3.1 变成 3,3.6 变成 4,中间值 3.5 变成 4。
js知识回顾-对象拷贝
- slice()方法返回一个新的数组对象,这一对象是一个由
begin和end决定的原数组的浅拷贝(包括begin,不包括end)。原始数组不会被改变。
最后,修改Game组件的render函数,在历史记录<li>标签中写入落棋位置。
render() {
//...省略其他代码
let history = this.state.history.slice(0, this.state.stepNumber + 1);
const moves = this.state.history.map((step,move)=>{
const desc = move?'go to move # '+move:'go to game start';
const pos = move?'row:'+this.state.history[move].position.row+',col:'+this.state.history[move].position.col:'';
return (
<li key={move}>{pos}<button onClick={()=>{this.jumpTo(move)}}>{desc}</button></li>
)
})
//...
}
2.在历史记录列表中加粗显示当前选择的项目
在渲染历史记录信息时,map循环中新增一步判断当前move是否等于stepNumber,如果是的话,就把pos信息放入加粗标签中显示。
const moves = this.state.history.map((step,move)=>{
const desc = move?'go to move # '+move:'go to game start';
const pos = move?'row:'+this.state.history[move].position.row+',col:'+this.state.history[move].position.col:'';
const posView = this.state.stepNumber === move?(<span className='bolder'>{pos}</span>):(<span>{pos}</span>);
return (
<li key={move}>{posView}<button onClick={()=>{this.jumpTo(move)}}>{desc}</button></li>
)
});
设置bolder类的css样式
.bolder {
font-weight: 900;
}
3.使用两个循环来渲染出棋盘的格子
首先修改Game组件的构造函数,初始化squares数组长度时动态赋值。然后修改render函数,向board组件传入棋盘的大小rowSize和colSize。
class Game extends React.Component{
boardSize= {rowSize: 3, colSize:3};
constructor(props){
super(props);
this.state = {
history: [{
squares: Array(this.boardSize.rowSize * this.boardSize.colSize).fill(null),
position: {row: -1, col: -1},
}],
xIsNext: true,
stepNumber: 0,
}
}
//...
render() {
//...
return (
<div className="game">
<div className="game-board">
<Board rowSize={this.boardSize.rowSize} colSize={this.boardSize.colSize} squares={cur.squares} onClick={(i)=>{this.handleClick(i)}} />
</div>
...
</div>
);
}
}
最后修改board组件的渲染逻辑,根据行列长度使用两个循环来完成。
class Board extends React.Component {
renderSquare(i) {
return (<Square key={i} value={this.props.squares[i]} onClick={() => { this.props.onClick(i)}} />);
}
render() {
const rows = [];
for(let i=0;i<this.props.rowSize;i++){
const row = [];
for(let j=0;j<this.props.colSize;j++){
row.push(this.renderSquare(this.props.colSize*i + j));
}
rows.push(
<div key={i} className="board-row">{row}</div>
);
}
return (
<div>{rows}</div>
);
}
}
4.添加一个可以升序或降序显示历史记录的按钮
首先在Game组件的state中维护一个名为isAsc的变量,记录当前的历史记录显示顺序。
constructor(props){
super(props);
this.state = {
history: [{
squares: Array(this.boardSize.rowSize * this.boardSize.colSize).fill(null),
position: {row: -1, col: -1},
}],
xIsNext: true,
stepNumber: 0,
isAsc: true,
}
}
然后修改render函数中历史记录的渲染逻辑。保存一个moves数组的反转数组reverseMoves,根据isAsc的值,切换在页面中显示的数据是moves还是reverseMoves。
changeSeq(){
this.setState({
isAsc: !this.state.isAsc,
})
}
render(){
//...
const reverseMoves = moves.slice().reverse();
const movesView = this.state.isAsc?moves:reverseMoves;
const seqButtonInfo = this.state.isAsc?"change to DESC":"change to ASC";
return (
<div className="game">
<div className="game-board">
<Board rowSize={this.boardSize.rowSize} colSize={this.boardSize.colSize} squares={cur.squares} onClick={(i)=>{this.handleClick(i)}} />
</div>
<div className="game-info">
<div>{status}</div>
<button onClick={()=>{this.changeSeq()}}>{seqButtonInfo}</button>
<ol>{movesView}</ol>
</div>
</div>
);
}
5.每当有人获胜时,高亮显示连成一线的 3 颗棋子
在history数组中新增一个名为winSquares的数组变量,记录棋盘上的相应位置的状态,连成线的三个棋子上的值设置为true。
constructor(props){
super(props);
this.state = {
history: [{
squares: Array(this.boardSize.rowSize * this.boardSize.colSize).fill(null),
winSquares: Array(this.boardSize.rowSize * this.boardSize.colSize).fill(false),
position: {row: -1, col: -1},
}],
xIsNext: true,
stepNumber: 0,
isAsc: true,
}
}
修改handleClick()函数,落棋时复制一份winSquares。
handleClick(i) {
//...
const winSquares = cur.winSquares.splice();
this.setState({
history: history.concat({squares,winSquares,position}),
xIsNext: !this.state.xIsNext,
stepNumber: this.state.stepNumber+1,
});
//...
}
修改calculateWinner()函数,返回获胜方以及连起来的三个棋子的位置。
function calculateWinner(squares) {
//...
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 {winner:squares[a],squares:[a,b,c]};
}
}
return null;
}
修改Game组件的render()函数,当有人获胜时,修改winSquares数组的状态。
render(){
const winner = calculateWinner(cur.squares);
if(winner){
status = 'Winner:' + winner.winner;
cur.winSquares[winner.squares[0]] = true;
cur.winSquares[winner.squares[1]] = true;
cur.winSquares[winner.squares[2]] = true;
}
}
最后修改各组件的渲染逻辑,根据isWin值控制显示样式。
function Square(props) {
const valueView = props.value==null?null:props.isWin?(<span className='winSquare'>{props.value}</span>):(<span>{props.value}</span>);
return (
<button className="square" onClick={() => { props.onClick() }}>
{valueView}
</button>
);
}
<Board
rowSize={this.boardSize.rowSize}
colSize={this.boardSize.colSize}
squares={cur.squares}
winSquares={cur.winSquares}
onClick={(i)=>{this.handleClick(i)}}
/>
renderSquare(i) {
return (
<Square
key={i}
value={this.props.squares[i]}
isWin={this.props.winSquares[i]}
onClick={() => { this.props.onClick(i)}}
/>
);
}
设置winSquare类的css样式。
.winSquare{
color: red;
}
6.当无人获胜时,显示一个平局的消息
修改Game组件的render()函数,当前无人获胜时,判断stepNumber是否等于9,是的话说明当前棋盘已下满,该局为平局。
if(winner){
status = 'Winner:' + winner.winner;
cur.winSquares[winner.squares[0]] = true;
cur.winSquares[winner.squares[1]] = true;
cur.winSquares[winner.squares[2]] = true;
}
else if(this.state.stepNumber === 9){
status = 'no one wins';
}
else{
status = 'Next player: '+ (this.state.xIsNext?'X':'O');
}