React从入门到入土系列2-实战:井字游戏

这是我自己系统整理的React系列博客,主要参考2023年3开放的最新版本react官网内容,欢迎你阅读本系列内容,希望能有所收货。

本文是该系列第2篇文章,阅读完本文后你将收获:

  • 使用React编写一个井字游戏
  • 通过实战的方式加深你对React的理解

1 说明

本文是一个实战项目,需要使用React编写一个井字游戏,在这之前先解释一些基础的javascript语法,如果你已经对javascript了如指掌,可以直接跳过这一小节。

你可以在这个Codesandbox链接中完成这次代码的编写。

App.js

App.js中有如下代码:

export default function Square() {
    
    
  return <button className="square">X</button>;
}

这段代码定义了一个Squre组件,export关键字让Square函数能够被其他文件中的代码调用,default关键字告诉其他文件Square这个方法是这个文件中主要的方法。

styles.css

点击左侧导航栏中的styles.css,这个文件里面定义了React APP的样式代码。

index.js

点击左侧导航栏中的index.js,这个文件是项目的入口,已经引用了App.js,你不需要在这次的实战中修改该文件。

2 开始编写基础代码

下面开始编写基础代码,我们在App.js中完善基础组件Square,该组件的props有两项内容:

  1. value指示当前方格中的值
  2. onClick是当前方格被点击后传递给父组件的事件处理函数
export function Square(props) {
    
    
  return <button className="square" onClick={
    
    props.onClick}>{
    
    props.value}</button>;
}

然后定义Board组件,作为主入口:

import {
    
     useState } from "react";

export default function Board() {
    
    
  // 使用state保存棋盘的状态,由于一共3x3,因此使用一个长度为9的数组保存状态
  const [board, setBoard] = useState(Array(9).fill(null));

  // 各个方格点击后的处理函数,由于需要知道是点击的哪个方格,因此需要传入下标
  // 点击了方格之后,就将对应的取值设置为“x”,然后更新棋盘
  const handleClick = (i) => {
    
    
    const nextBoard = board.slice();
    nextBoard[i] = "X";
    setBoard(nextBoard);
  };

  return (
    <>
      <div className="board-row">
        <Square value={
    
    board[0]} onClick={
    
    () => handleClick(0)} />
        <Square value={
    
    board[1]} onClick={
    
    () => handleClick(1)} />
        <Square value={
    
    board[2]} onClick={
    
    () => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={
    
    board[3]} onClick={
    
    () => handleClick(3)} />
        <Square value={
    
    board[4]} onClick={
    
    () => handleClick(4)} />
        <Square value={
    
    board[5]} onClick={
    
    () => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={
    
    board[6]} onClick={
    
    () => handleClick(6)} />
        <Square value={
    
    board[7]} onClick={
    
    () => handleClick(7)} />
        <Square value={
    
    board[8]} onClick={
    
    () => handleClick(8)} />
      </div>
    </>
  );
}

至此,我们已经把基础的棋盘搭建出来了,而且点击了每个方格都会将该方格设置为x
在这里插入图片描述

扫描二维码关注公众号,回复: 16005399 查看本文章

复盘

到目前为止,我们遇到了一些问题,总结如下:

问题1 <Square value={board[0]} onClick={() => handleClick(0)} />为什么不直接写成handleClick(0) 呢?

答:如果直接写handleClick(0)相当于调用了该函数,而该函数又修改了Board组件中的state,会导致react组件重新渲染。重新渲染之后,又会默认执行handleClick(0)从而导致死循环,因此需要写成() => handleClick(0)

问题2:为什么在handleClick函数内,要使用const nextBoard = board.slice();创建一份原始数组的拷贝呢?
答:为了解释这个问题,我们需要讨论一下不可变性(immutability)。更改数据通常有两种方法。第一种方法是通过直接改变数据的值来改变数据。第二种方法是用具有所需更改的新副本替换数据。这是如果你改变平方数组的样子:

const board = [null, null, null, null, null, null, null, null, null];
board[0] = 'X';
// Now `board` is ["X", null, null, null, null, null, null, null, null];

这是定义了一个新的nextSquare数组的样子:

const board = [null, null, null, null, null, null, null, null, null];
const nextBoard = ['X', null, null, null, null, null, null, null, null];
// Now `board` is unchanged, but `nextBoard` first element is 'X' rather than `null`

结果是相同的,但是通过不直接改变(改变底层数据),你可以获得几个好处:

  • **不可变性使得复杂的功能更容易实现。**在下文中,你将实现一个编辑历史功能,让你回顾游戏的历史,并跳回过去的动作。这个功能并不是游戏特有的,撤销和重做某些操作的能力是应用程序的常见需求。避免直接的数据突变可以让你保持以前版本的数据不变,并在以后重用它们。
  • 默认情况下,当父组件的状态发生变化时,所有子组件都会自动重新渲染。这甚至包括不受更改影响的子组件。尽管重新渲染本身对用户来说并不明显,但出于性能原因,你可能希望跳过重新渲染树中明显不受其影响的部分。不可变性使得组件比较它们的数据是否改变的成本非常低。你可以在memo API中了解React如何选择何时重新呈现组件。

3 完善游戏逻辑

接下来,我们完善Board组件中的逻辑,让整个游戏可以正常运行起来。

轮流填写

井字游戏一般由两名玩家轮流填写,第一名玩家填写X,第二名玩家填写O,如此循环直至游戏结束。因此需要设置一个state记录当前是谁在填写。

export default function Board() {
    
    
  const [board, setBoard] = useState(Array(9).fill(null));
  const [nowTurn, setNowTurn] = useState(true);
  
  const handleClick = (i) => {
    
    
    const nextBoard = board.slice();
    // 添加判断代码,如果已经填写过的位置不能填写
    if (!nextBoard[i]) {
    
    
      nextBoard[i] = nowTurn ? "X" : "O";
    }
    setBoard(nextBoard);
    setNowTurn(!nowTurn)
  };

  return (
    <>
      <div className="board-row">
        <Square value={
    
    board[0]} onClick={
    
    () => handleClick(0)} />
        <Square value={
    
    board[1]} onClick={
    
    () => handleClick(1)} />
        <Square value={
    
    board[2]} onClick={
    
    () => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={
    
    board[3]} onClick={
    
    () => handleClick(3)} />
        <Square value={
    
    board[4]} onClick={
    
    () => handleClick(4)} />
        <Square value={
    
    board[5]} onClick={
    
    () => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={
    
    board[6]} onClick={
    
    () => handleClick(6)} />
        <Square value={
    
    board[7]} onClick={
    
    () => handleClick(7)} />
        <Square value={
    
    board[8]} onClick={
    
    () => handleClick(8)} />
      </div>
    </>
  );
}

如此一来,就能够轮流填写了:
在这里插入图片描述

计算赢家

在每一此填写之后,都应该判断游戏是否结束(同一行、列或者对角线全部为同一种标记)。因此接下来实现游戏状态判断,并且添加一个status字段,用于提示当前游戏状态。

export default function Board() {
    
    
  const [board, setBoard] = useState(Array(9).fill(null));
  const [nowTurn, setNowTurn] = useState(true);

  const handleClick = (i) => {
    
    
    // 如果该位置已经被填写,或者游戏已经结束,不执行后续操作
    if (board[i] || calculateWinner()) {
    
    
      return;
    }
    const nextBoard = board.slice();
    nextBoard[i] = nowTurn ? "X" : "O";
    setBoard(nextBoard);
    setNowTurn(!nowTurn);
  };

  const calculateWinner = () => {
    
    
    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 (board[a] && board[a] === board[b] && board[a] === board[c]) {
    
    
        return board[a];
      }
    }
    return null;
  };

  const winner = calculateWinner();
  // 使用status记录当前游戏状态
  // 注意:status可以不用state存储!
  let status = winner
    ? "Winner: " + winner
    : "Next player: " + (nowTurn ? "X" : "O");

  return (
    <>
      <div className="status">{
    
    status}</div>
      <div className="board-row">
        <Square value={
    
    board[0]} onClick={
    
    () => handleClick(0)} />
        <Square value={
    
    board[1]} onClick={
    
    () => handleClick(1)} />
        <Square value={
    
    board[2]} onClick={
    
    () => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={
    
    board[3]} onClick={
    
    () => handleClick(3)} />
        <Square value={
    
    board[4]} onClick={
    
    () => handleClick(4)} />
        <Square value={
    
    board[5]} onClick={
    
    () => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={
    
    board[6]} onClick={
    
    () => handleClick(6)} />
        <Square value={
    
    board[7]} onClick={
    
    () => handleClick(7)} />
        <Square value={
    
    board[8]} onClick={
    
    () => handleClick(8)} />
      </div>
    </>
  );
}

运行结果如下图:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_26822029/article/details/129720164