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

news/2024/6/15 13:52:47 标签: react.js, 游戏, javascript

这是我自己系统整理的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
在这里插入图片描述

复盘

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

问题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>
    </>
  );
}

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


http://www.niftyadmin.cn/n/176677.html

相关文章

【Linux】安装DHCP服务器

1、先检测网络是否通 get dhcp.txt rpm -qa //查看软件包 rpm -qa |grep dhcp //确定是否安装 yum install dhcp //进行安装 安装完成后 查询 rpm -ql dhcp 进行配置 cd /etc/dhcp 查看是否有遗留dhcpd.conf.rpmsave 删除该文件 cp /usr/share/doc/dhcp-4.1.1/dhcpd.conf.sampl…

Docker容器化技术

文章目录为什么要学习容器化技术人们遇到了什么问题虚拟化技术Docker容器化技术应用概述Docker的C/S模式简单使用docker核心组件&#xff1a;Images(镜像)Containers(容器)Registry容器网络&#xff08;docker network&#xff09;概述1、docker网络实现的原理2、容器的端口映射…

如何通过命令行查看CentOS版本信息和linux系统信息

1.如何查看已安装的CentOS版本信息&#xff1a; 1.cat /proc/version 2.uname -a 3.uname -r 4.cat /etc/centos-release 5.lsb_release -a 6.hostnamectl1. 第一种方式输出的结果是&#xff1a; Linux version 3.10.0-1127.el7.x86_64 (mockbuildkbuilder.bsys.centos.org) …

Android Studio 1.点击按钮切换界面

页面布局xml文件activity_main&#xff1a; <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height&…

LFM雷达实现及USRP验证【章节2:LFM雷达测距】

目录 1. 参数设计 几个重要的约束关系 仿真参数设计 2. matlab雷达测距代码 完整源码 代码分析 回顾&#xff1a;LFM的基本原理请详见第一章 本章节将介绍LFM雷达测距的原理及实现 1. 参数设计 几个重要的约束关系 带通采样定理&#xff1a; 因此如果我们B80MHz时&a…

可做题2(矩阵快速幂,乘法逆元,exgcd)

题目链接&#xff1a;可做题2 (nowcoder.com) 题目描述 若一个数列a满足条件anan-1an-2,n ≥ 3,而a1,a2为任意实数&#xff0c;则我们称这个数列为广义斐波那契数列。 现在请你求出满足条件a1i&#xff0c;a2为区间[l,r]中的整数&#xff0c;且ak mod pm的广义斐波那契数列有…

为了之后找工作不被虐,每天刷3道《剑指offer》Day-1

本文已收录于专栏&#x1f33b;《刷题笔记》文章目录前言&#x1f496; 1、二维数组中的查找题目描述思路&#x1f496; 2、替换空格题目描述思路&#x1f496; 3、从尾到头打印链表题目描述思路一&#xff08;反转函数&#xff09;思路二&#xff08;递归&#xff09;思路二&a…

快排,必拿下[java代码]

前导课划分成<P区和>P区以最后一个元素作为划分值---假设为P&#xff0c;将整个数组划分成[ 小于等于P的区域 大于P的区域]每个区域里无序。操作流程&#xff1a;从左往右遍历&#xff0c;有两个分支逻辑①若当前数<P,则当前数和小于等于区的下一数交换&#xff0c;小…