跳至主要內容

30日


30日

React的教程:井字棋游戏

https://zh-hans.react.dev/learn/tutorial-tic-tac-toeopen in new window

import { useState } from 'react';

function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

function Board({ xIsNext, squares, onPlay }) {
  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = 'X';
    } else {
      nextSquares[i] = 'O';
    }
    onPlay(nextSquares);
  }

  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
  }

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

export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove];

  function handlePlay(nextSquares) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }

  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
  }

  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = 'Go to move #' + move;
    } else {
      description = 'Go to game start';
    }
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </div>
  );
}

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;
}

代码讲解

这段代码是一个使用React框架编写的简单井字棋游戏的实现。游戏由一个3x3的棋盘和两个玩家(通常用'X'和'O'表示)组成。每个玩家轮流在棋盘上的空格(square)点击,以尝试连成一线(水平、垂直或对角线)从而赢得游戏。 下面是代码的主要组成部分和它们的功能:

  1. Square 组件:这是一个简单的按钮组件,用于在棋盘上显示玩家的棋子('X'或'O')。
  2. Board 组件:这个组件负责渲染整个棋盘。它接收当前玩家的棋子('X'或'O'),当前棋盘的状态(一个包含9个空格值的数组),以及一个当玩家点击棋盘上的空格时应该调用的函数(onPlay)。
    • handleClick 函数:当玩家点击棋盘上的一个空格时调用。它首先检查该位置是否已被占用或游戏是否已有赢家。如果没有,它会在棋盘数组的相应位置更新玩家的棋子,并调用onPlay函数来更新游戏状态。
    • calculateWinner 函数:检查棋盘上是否有玩家连成一线。它通过检查棋盘上的所有可能的线(水平、垂直、对角线)来确定是否有赢家,并返回赢家的棋子('X'或'O')。如果没有赢家,它返回null
  3. Game 组件:这是游戏的主要组件,它管理游戏的历史记录和当前状态。
    • 它使用useState来跟踪游戏历史(history),当前移动(currentMove),以及哪个玩家的回合(xIsNext)。
    • handlePlay 函数:当玩家在棋盘上做出移动时调用。它将当前移动的棋盘状态添加到游戏历史中,并更新当前移动。
    • jumpTo 函数:允许玩家跳转到游戏历史中的特定移动。
    • moves 是一个数组,包含游戏历史中每个移动的描述。它使用map函数来创建一个包含按钮的列表,玩家可以点击这些按钮来回顾游戏的每一步。
  4. calculateWinner 函数(在Game组件外部定义):这是之前在Board组件中提到的函数,用于确定是否有玩家赢得了游戏。

整个游戏的UI由Game组件组织,它显示当前棋盘的状态(通过Board组件),以及游戏历史(通过一个有序列表显示)。 玩家可以通过点击棋盘上的空格来进行游戏,也可以通过点击历史列表中的按钮来回看之前的移动。游戏的主要逻辑,如检查赢家和更新棋盘状态,都封装在组件的函数和方法中。

需要注意的1

在改变棋盘状态时,并没有直接修改 squares ,而是使用 slice() 复制了一份,这样做的好处是:

const nextSquares = squares.slice();
if (xIsNext) {
    nextSquares[i] = 'X';
} else {
    nextSquares[i] = 'O';
}
onPlay(nextSquares);

在JavaScript中,数组是按引用传递的,这意味着当你将一个数组赋值给另一个变量时,两个变量实际上指向内存中的同一个数组对象。因此,如果你修改了这个数组,所有指向这个数组的变量都会反映这些更改。

在React组件Board中,squares是一个从父组件Game通过props传递进来的数组。为了不在原始数组上直接进行更改,避免影响到其他可能使用这个原始数组的组件或逻辑,我们需要创建这个数组的一个副本来进行修改。

squares.slice()方法在这里被用来创建squares数组的一个浅拷贝(shallow copy)。这意味着我们创建了一个新的数组对象,它包含了原始数组中的所有元素的引用。当我们修改这个新数组的元素时,不会影响原始数组,因为我们只是修改了副本,而不是原始数组本身。

这样做的好处是:

  1. 保持不可变性(Immutability):在React中,保持状态的不可变性是一种最佳实践。通过创建状态的副本并修改副本,而不是直接修改状态,我们可以确保组件的渲染是基于最新的、不变的状态。
  2. 避免副作用(Side Effects):不在原始数组上直接进行更改可以避免潜在的副作用,这些副作用可能会导致难以追踪的错误,尤其是在复杂的应用中。
  3. 清晰的数据流(Clear Data Flow):通过使用slice()创建副本,我们可以清晰地追踪数据是如何在组件之间流动的,以及哪些操作导致了数据的变化。

handleClick函数中,我们使用nextSquares[i] = 'X'nextSquares[i] = 'O'来更新副本数组的特定元素。然后,我们通过handlePlay函数将这个更新后的数组传递给Game组件,并通过setHistory更新游戏的历史记录。这样,我们就可以在不影响原始squares数组的情况下,安全地更新游戏状态。

需要注意的2

使用 handleClick(0) 是行不通的,因为这样直接触发了 handleClick,而不是点击触发

<Square value={squares[0]} onSquareClick={handleClick(0)} />

应该改成 () => handleClick(0)

<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
上次编辑于:
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度