React入门教程之井字棋(三)——游戏完善

2023-11-10

我们现在已经编写好了井字棋游戏中,最基础的可以落子的棋盘。为了开发一个完整的游戏,我们还需要交替在棋盘上放置 “X” 和 “O”,并且判断出胜者。


状态提升

当前,每个 Square 组件都维护了游戏的状态。我们可以把所有 9 个 Square 的值放在一个地方,这样我们就可以判断出胜者了。

你可能会想,我们也可以在棋盘 Board 组件中收集每个格子 Square 组件中的 state。虽然技术上来讲是可以实现的,但是代码如此编写会让人很难理解,并且我们以后想要维护重构时也会非常困难。所以,最好的解决方式是直接将所有的 state 状态数据存储在 Board 父组件当中。之后 Board 组件可以将这些数据通过 props 传递给各个 Square 子组件,正如上文我们把数字传递给每一个 Square 一样。

当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,需要把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中所有组件的状态数据就能够更方便地同步共享了。

像这种将组件的 state 提升到父组件的情形在重构 React 组件时经常会遇到 —— 借此我们来实践一下。

为 Board 组件添加构造函数,将 Board 组件的初始状态设置为长度为 9 的空值数组:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  renderSquare(i) {
    return <Square value={i} />;
  }

当我们填充棋盘后,this.state.squares 数组的值可能如下所示:

[
  'O', null, 'X',
  'X', 'X', 'O',
  'O', null, null,
]

Board 组件当前的 renderSquare 方法看起来像下面这样:

 renderSquare(i) {
    return <Square value={i} />;
  }

开始时,我们依次使把 0 到 8 的值通过 prop 从 Board 向下传递,从而让它们显示出来。上一步与此不同,我们根据 Square 自己内部的 state,使用了 “X” 来代替之前的数字。因此,Square 忽略了当前从 Board 传递给它的那个 valueprop。

让我们再一次使用 prop 的传递机制。我们通过修改 Board 来指示每一个 Square 的当前值('X', 'O', 或者 null)。我们在 Board 的构造函数中已经定义好了 squares 数组,这样,我们就可以通过修改 Board 的 renderSquare 方法来读取这些值了。

  renderSquare(i) {
    return <Square value={this.state.squares[i]} />;
  }

这样,每个 Square 就都能接收到一个 value prop 了,这个 prop 的值可以是 'X''O'、 或 nullnull 代表空方格)。

接下来,我们要修改一下 Square 的点击事件监听函数。Board 组件当前维护了那些已经被填充了的方格。我们需要想办法让 Square 去更新 Board 的 state。由于 state 对于每个组件来说是私有的,因此我们不能直接通过 Square 来更新 Board 的 state。

相反,从 Board 组件向 Square 组件传递一个函数,当 Square 被点击的时候,这个函数就会被调用。接着,我们将 Board 组件的 renderSquare 方法改写为如下效果:

renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

注意
为了提高可读性,我们把返回的 React 元素拆分成了多行,同时在最外层加了小括号,这样 JavaScript 解析的时候就不会在 return 的后面自动插入一个分号从而破坏代码结构了。

现在我们从 Board 组件向 Square 组件中传递两个 props 参数:valueonClickonClick prop 是一个 Square 组件点击事件监听函数。接下来,我们需要修改 Square 的代码:

  • 将 Square 组件的 render 方法中的 this.state.value 替换为 this.props.value
  • 将 Square 组件的 render 方法中的 this.setState() 替换为 this.props.onClick()
  • 删掉 Square 组件中的构造函数 constructor,因为该组件不需要再保存游戏的 state。

进行上述修改之后,代码会变成下面这样:

class Square extends React.Component {
  render() {
    return (
      <button
        className="square"
        onClick={() => this.props.onClick()}
      >
        {this.props.value}
      </button>
    );
  }
}

每一个 Square 被点击时,Board 提供的 onClick 函数就会触发。我们回顾一下这是怎么实现的:

  1. 向 DOM 内置元素 <button> 添加 onClick prop,让 React 开启对点击事件的监听。
  2. 当 button 被点击时,React 会调用 Square 组件的 render() 方法中的 onClick 事件处理函数。
  3. 事件处理函数触发了传入其中的 this.props.onClick() 方法。这个方法是由 Board 传递给 Square 的。
  4. 由于 Board 把 onClick={() => this.handleClick(i)} 传递给了 Square,所以当 Square 中的事件处理函数触发时,其实就是触发的 Board 当中的 handleClick(i) 方法。
  5. 现在我们还尚未定义 handleClick() 方法,所以代码还不能正常工作。如果此时点击 Square,你会在屏幕上看到红色的错误提示,提示内容为:“this.handleClick is not a function”。

注意
因为 DOM 元素 <button> 是一个内置组件,因此其 onClick 属性在 React 中有特殊的含义。而对于用户自定义的组件来说,命名就可以由用户自己来定义了。我们给 Square 的 onClick 和 Board 的 handleClick 赋予任意的名称,代码依旧有效。在 React 中,有一个命名规范,通常会将代表事件的监听 prop 命名为 on[Event],将处理事件的监听方法命名为 handle[Event] 这样的格式。

这时候我们点击 Square 的时候,浏览器会报错,因为我们还没有定义 handleClick 方法。我们现在来向 Board 里添加 handleClick 方法:

 handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

现在,我们可以通过点击 Square 来填充那些方格,效果与之前相同。但是,当前 state 没有保存在单个的 Square 组件中,而是保存在了 Board 组件中。每当 Board 的 state 发生变化的时候,这些 Square 组件都会重新渲染一次。把所有 Square 的 state 保存在 Board 组件中可以让我们在将来判断出游戏的胜者。

因为 Square 组件不再持有 state,因此每次它们被点击的时候,Square 组件就会从 Board 组件中接收值,并且通知 Board 组件。在 React 术语中,我们把目前的 Square 组件称做“受控组件”。在这种情况下,Board 组件完全控制了 Square 组件。

注意,我们调用了 .slice() 方法创建了 squares 数组的一个副本,而不是直接在现有的数组上进行修改。在下一节,我们会介绍为什么我们需要创建 square 数组的副本。


为什么不可变性在 React 中非常重要

在上一节内容当中,我们建议使用 .slice() 函数对 square 数组进行拷贝,而非直接修改现有的数组。接下来我们来学习不可变性以及不可变性的重要性。

一般来说,有两种改变数据的方式。第一种方式是直接修改变量的值,第二种方式是使用新的一份数据替换旧数据。

直接修改数据

var player = {score: 1, name: 'Jeff'};
player.score = 2;
// player 修改后的值为 {score: 2, name: 'Jeff'}

新数据替换旧数据

var player = {score: 1, name: 'Jeff'};

var newPlayer = Object.assign({}, player, {score: 2});
// player 的值没有改变, 但是 newPlayer 的值是 {score: 2, name: 'Jeff'}

// 使用对象展开语法,就可以写成:
// var newPlayer = {...player, score: 2};

不直接修改(或改变底层数据)这种方式和前一种方式的结果是一样的,这种方式有以下几点好处:

  1. 简化复杂的功能
    不可变性使得复杂的特性更容易实现。在后面的章节里,我们会实现一种叫做“时间旅行”的功能。“时间旅行”可以使我们回顾井字棋的历史步骤,并且可以“跳回”之前的步骤。这个功能并不是只有游戏才会用到——撤销和恢复功能在开发中是一个很常见的需求。不直接在数据上修改可以让我们追溯并复用游戏的历史记录。
  2. 跟踪数据的改变
    如果直接修改数据,那么就很难跟踪到数据的改变。跟踪数据的改变需要可变对象可以与改变之前的版本进行对比,这样整个对象树都需要被遍历一次。
    跟踪不可变数据的变化相对来说就容易多了。如果发现对象变成了一个新对象,那么我们就可以说对象发生改变了。
  3. 确定在 React 中何时重新渲染
    不可变性最主要的优势在于它可以帮助我们在 React 中创建 pure components。我们可以很轻松的确定不可变数据是否发生了改变,从而确定何时对组件进行重新渲染。

函数组件

接下来我们把 Square 组件重写为一个函数组件

如果你想写的组件只包含一个 render 方法,并且不包含 state,那么使用函数组件就会更简单。我们不需要定义一个继承于 React.Component 的类,我们可以定义一个函数,这个函数接收 props 作为参数,然后返回需要渲染的元素。函数组件写起来并不像 class 组件那么繁琐,很多组件都可以使用函数组件来写。

把 Square 类替换成下面的函数:

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

我们把两个 this.props 都替换成了 props

注意
当我们把 Square 修改成函数组件时,我们同时也把 onClick={() => this.props.onClick()} 改成了更短的 onClick={props.onClick}(注意两侧都没有括号)。

轮流落子

现在井字棋还有一个明显的缺陷有待完善:目前还不能在棋盘上标记 “O”。

我们将 “X” 默认设置为先手棋。你可以通过修改 Board 组件的构造函数中的初始 state 来设置默认的第一步棋子:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

棋子每移动一步,xIsNext(布尔值)都会反转,该值将确定下一步轮到哪个玩家,并且游戏的状态会被保存下来。我们将通过修改 Board 组件的 handleClick 函数来反转 xIsNext 的值:

 handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

修改之后,我们就实现了 “X” 和 “O” 轮流落子的效果。尝试玩一下。

接下来修改 Board 组件 render 方法中 “status” 的值,这样就可以显示下一步是哪个玩家的了。

 render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

    return (
      // 其他部分没有改变

现在你整个的 Board 组件的代码应该是下面这样的:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

  render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

判断出胜者

至此我们就可以看出下一步会轮到哪位玩家,与此同时,我们还需要显示游戏的结果来判定游戏结束。拷贝如下 calculateWinner 函数并粘贴到文件底部:

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

传入长度为 9 的数组,此函数将判断出获胜者,并根据情况返回 “X”,“O” 或 “null”。

接着,在 Board 组件的 render 方法中调用 calculateWinner(squares) 检查是否有玩家胜出。一旦有一方玩家胜出,就把获胜玩家的信息显示出来,比如,“胜者:X” 或者“胜者:O”。现在,我们把 Board 的 render 函数中的 status 的定义修改为如下代码:

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

    return (
      // 其他部分没有修改

最后,修改 handleClick 事件,当有玩家胜出时,或者某个 Square 已经被填充时,该函数不做任何处理直接返回。

  handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

恭喜!现在你已经完成了井字棋!除此之外,你也已经掌握了 React 的基本常识。所以坚持到这一步的你才是真正的赢家呀!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

React入门教程之井字棋(三)——游戏完善 的相关文章

随机推荐

  • flink程序运行问题

    异常 kafka分区连接超时 phoinex启动阻塞 发现hbase起不来 hdfs报错Please check the logs or run fsck in order to identify the missing blocks 解决
  • ntp服务器是什么,有什么用?

    https www idcbest com servernews 11002256 html ntp服务器隶属于小众时间频率行业 对于初次接触者和未涉猎此行业的人群来说 就会有很大的疑问 ntp服务器能做什么 为什么会需要ntp服务器 本文
  • 用Python爬取微信好友签名并生成词云(解决词云白板问题)

    今天上班看到一篇关于用Python爬取微信好友签名并生成词云的文章 觉得很有趣 就学着尝试爬取自己的微信好友签名 菜鸟写代码 总是会出现很多问题 摸索了一天 终于成功了 记录下自己的经过 希望能给像自己一样的小白一点点参考 本博客代码参考h
  • neon 指令 c语言,NEON初步使用

    前言 指令集并行是CPU的优化加速的一个方向 在ARM芯片主要是利用NEON指令集实现指令集并行 NEON简介 NEON就是高级SIMD 单指令多数据 适用于图像 音频等数据处理 ARMv6就叫SIMD ARMv7开始叫NEON aarch
  • 前端JS笔记

    JS笔记 持续更新中
  • c语言string函数作用,浅谈C语言之字符串处理函数

    下面介绍8种基本的常用的字符串处理函数 在数值数组中也常常用到 部分函数 所有的C语言编译系统中一般都提供这些函数 1 puts函数 输出字符串的函数 一般的形式为puts 字符串组 作用 将一个字符串输出到终端 如 char一个strin
  • CAD卸载/完美解决安装失败/如何彻底卸载清除干净cad各种残留注册表和文件的方法...

    在卸载cad重装CAD时发现安装失败 提示是已安装或安装失败 这是因为上一次卸载后没有清理干净 系统会误认为已经安装过了 有的同学是新装的系统也会出现安装失败的情况 这是因为C 或者 NET的原因 无论任何版本的cad在手动删除卸载之后都会
  • nginx查看php错误日志,nginx php-fpm输出php错误日志的方法

    nginx php fpm输出php错误日志的方法 发布时间 2020 08 15 11 03 12 来源 亿速云 阅读 137 作者 小新 nginx php fpm输出php错误日志的方法 这个问题可能是我们日常学习或工作经常见到的 希
  • B+树结构与索引<一> _ 结构与索引

    目录 一 B 树结构 1 二分查找法 2 二叉查找树 3 平衡二叉树 4 平衡多路查找树 B Tree 5 B 树 二 操作B 树 1 插入操作 2 删除操作 三 B 树索引类型 1 聚集索引 clustered index 2 辅助索引
  • == 和 equals 的区别是什么

    解读 对于基本类型和引用类型 的作用效果是不同的 如下所示 基本类型 比较的是值是否相同 引用类型 比较的是引用是否相同 equals 解读 equals 本质上就是 只不过 String 和 Integer 等重写了 equals 方法
  • 给你的类重写Equals--检测Class是否相等

    在C 的容器中 常用的三个容器数组 ArrayList Hashtable 数组比较简单 实现某种单一数据的存储 但是并不能自由插入 移除和容纳不同的对象 所以ArrayList是数组的替代品 并且由于ArrayList可以自由的添加 删除
  • 调用接口时 net::ERR_CERT_AUTHORITY_INVALID

    调用接口控制台报错net ERR CERT AUTHORITY INVALID network栏也是红色 这种一般的情况是证书不被浏览器认可 检查下证书 我的情况是我在本地模拟的https 生成的模拟证书 所以是不被浏览器认可的 解决方案
  • centos-6.8下载与安装

    一 centos的下载 有了需要自己才会去动手 算是配置开发环境的一些记录吧 首先进入官网 https www centos org download 官网页面 全英文的界面 英语不是很好 但容易找到 list of current mir
  • PROFINET工业以太网教程---GSDML文件详解

    前面的文章 PROFINET工业以太网教程 10 GSD文件 我们介绍过GSD文件 它的全称是 General Station Description 中文翻译为 通用站描述文件 GSD文件的主要作用是对PROFINET或PROFIBUS子
  • java课设带app_IPAssignApp.java

    package tsinghuaip import javax swing UIManager import java awt public class IPAssignApp boolean packFrame false Constru
  • Java 描述将数字金额转换为中文大写

    Java 描述金额转换 数字转换成中文大写 解题思路 把每一位转换成对应的大写 然后在不足地方补零 最后加上相应单位 代码如下 import java util Scanner public class Main public static
  • 入门汇编(简单程序设计)

    将TABLE单元的10个字节数据传送到TABLE 5开始的单元 MOV CX 10 LEA SI TABLE LEA DI TABLE ADD DI 14 ADD SI 9 STD REP MOVSB 计算 X Y X 结果存Z单元 商是A
  • 多态的定义及其实现

    1 什么是多态性 多态性可以简单地概括为 一个接口 多种方法 程序在运行时才决定调用的函数 它是面向对象编程领域的核心概念 只有重写虚函数才体现C 的多态性 虚函数 虚函数对于多态具有决定性的作用 有虚函数才能构成多态 只需要在虚函数的声明
  • 您选择的文件不是有效的iso映像文件,请重新选择

    安装windows系统的时候无非就是参考类似于下面的这些博文 通用PE u盘装Ghost Win10系统教程http www tongyongpe com win10ghost html 用U盘装机大师安装GHOST WIN10系统http
  • React入门教程之井字棋(三)——游戏完善

    我们现在已经编写好了井字棋游戏中 最基础的可以落子的棋盘 为了开发一个完整的游戏 我们还需要交替在棋盘上放置 X 和 O 并且判断出胜者 状态提升 当前 每个 Square 组件都维护了游戏的状态 我们可以把所有 9 个 Square 的值