React 从零开始学习(九)—— 落子有悔

2023-10-30

到目前为止,demo 的操作是不能回退的,点击格子以后,想要记录历史的操作,就需要 使用 slice() 函数为每一步创建 squares 数组的副本,同时把这个数组当作不可变对象。这样就可以把所有 squares 数组的历史版本都保存下来了,然后也可以在历史的步骤中随意跳转。

由于在 Game 组件要展示一个历史步骤的列表,这个功能需要访问 history 的数据,因此需要把 state 放在顶层 Game 组件中;而要保存历史数据,则需要在 handleClick 函数中做处理,需要在 Game 组件中实现 handleClick 方法。

接下来,删除 Board 中的 state 以及 handleClick 处理函数,修改 Board 组件:

import React, { Component } from 'react'
import Square from './Square'
class Board extends Component {
  renderSquare (i) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)} />
    )
  }
  render () {
    return (
      <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>
    )
  }
}
export default Board

修改 Game.js 文件,在 render 中通过 calculateWinner 计算结果来展示当前游戏状态:

const history = this.state.history;   
const current = history[history.length - 1];   
const winner = calculateWinner(current.squares);    
let status;    
if (winner) {      
  status = 'Winner: ' + winner;    
} else {    
  status = 'Next player: ' + (this.state.isNext? 'X' : 'O');    
}

在 return 中添加div来展示状态:

    return (
      <div className="game">
        <div className="game-board">
          <Board 
          squares={current.squares}          
            onClick={(i) => this.handleClick(i)}          />     
        </div>
        <div className="game-info">
          <div>{status}</div>         
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );

修改 handleClick 方法:

  handleClick(i) {
    const history = this.state.history;   
    const current = history[history.length - 1];   
    const squares = current.squares.slice();    
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.isNext? 'X' : 'O';
    this.setState({
      history: history.concat([{        
      squares: squares,     
       }]),      
      isNext: !this.state.isNext
    })
  }

修改 Game 组件的 render 方法:

  render () {
    const history = this.state.history
    const current = history[history.length - 1]
    const winner = calculateWinner(current.squares)
    const moves = history.map((step, move) => {
      const text = move ? 'Go to move:' + move : 'Go to game start'
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{text}</button>
        </li>
      )
    })
    let status
    if (winner) {
      status = 'winner:' + winner
    } else {
      status = 'Next player:' + (this.state.isNext ? 'X' : 'O')
    }
    return (
      <div className="game-container">
        <div >
          <div>{status}</div>
          <ol className="step-item">{moves}</ol>
        </div>
        <div className="game">
          <div className="game-board">
            <Board squares={current.squares}
              onClick={(i) => this.handleClick(i)} />
          </div>
        </div>
       </div>
    )
  }
}

这里通过一个 包含按钮 元素的 li 的列表来展示历史步骤。

可以看到,上面的 li 标签,设置了 key 值。这是因为:

每当一个列表重新渲染时,React 会根据每一项列表元素的 key 来检索上一次渲染时与每个 key 所匹配的列表项。如果 React 发现当前的列表有一个之前不存在的 key,那么就会创建出一个新的组件。如果 React 发现和之前对比少了一个 key,那么就会销毁之前对应的组件。如果一个组件的 key 发生了变化,这个组件会被销毁,然后使用新的 state 重新创建一份。

key 是 React 中一个特殊的保留属性。当 React 元素被创建出来的时候,React 会提取出 key 属性,然后把 key 直接存储在返回的元素上。虽然 key 看起来好像是 props 中的一个,但是你不能通过 this.props.key 来获取 key。React 会通过 key 来自动判断哪些组件需要更新。组件是不能访问到它的 key 的。

所以,只要构建动态列表的时候,都要指定一个合适的 key。

在 state 中添加 stepNumber代表当前正在查看的那一项历史记录。

添加 jumpTo 方法,每次点击历史记录中的某一条时会触发 jumpTo 方法,更新 stepNumber 的值。

 jumpTo(step) {   
  this.setState({      
  stepNumber: step,      
  isNext: (step % 2) === 0,    
  }); 
}

修改 Game 组件的 handleClick 方法,每次点击格子时调用 setState 方法更新 stepNumber 。

handleClick (i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1)
    const current = history[history.length - 1]
    const squares = current.squares.slice()
    if (calculateWinner(squares) || squares[i]) {
      return
    }
    squares[i] = this.state.isNext ? 'X' : 'O'
    this.setState(
      {
        history: history.concat([{
          squares: squares
        }]),
        stepNumber: history.length,
        isNext: !this.state.isNext
      }
    )
    console.log('history', this.state.history)
    console.log('current', current)
  }

打印历史数据:

第一次点击格子:
在这里插入图片描述
第二次点击格子:
在这里插入图片描述
如上面两图所示,在 handleClick 中打印 history 历史数据,可以看到每一步的操作都被记录了下来,保存在 history 中了。

最后,修改 Geme 组件的 render 方法,根据当前 stepNumber 来渲染。

const history = this.state.history
const current = history[this.state.stepNumber]
const winner = calculateWinner(current.squares)

这样,点击任意一步,棋盘都会渲染那一步的棋子样式。

现在,棋盘可以记录每一步的操作,并可随意回退到之前的操作,如下图所示:
请添加图片描述

完整代码如下:

import React, { Component } from 'react'
import '../assets/css/Game.css'
import Board from './Board'
class Game extends Component {
  constructor(props) {
    super(props)
    this.state = {
      history: [{
        squares: Array(9).fill(null)
      }],
      stepNumber: 0,
      isNext: true
    }
  }
  handleClick (i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1)
    const current = history[history.length - 1]
    const squares = current.squares.slice()
    if (calculateWinner(squares) || squares[i]) {
      return
    }
    squares[i] = this.state.isNext ? 'X' : 'O'
    this.setState(
      {
        history: history.concat([{
          squares: squares
        }]),
        stepNumber: history.length,
        isNext: !this.state.isNext
      }
    )
  }
  jumpTo (step) {
    this.setState({
      stepNumber: step,
      isNext: (step % 2) === 0
    })
  }
  render () {
    const history = this.state.history
    const current = history[this.state.stepNumber]
    const winner = calculateWinner(current.squares)

    const moves = history.map((step, move) => {
      const text = move ? 'Go to move:' + move : 'Go to game start'
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{text}</button>
        </li>
      )
    })
    let status
    if (winner) {
      status = 'winner:' + winner
    } else {
      status = 'Next player:' + (this.state.isNext ? 'X' : 'O')
    }
    return (
      <div className="game-container">
        <div >
          <div>{status}</div>          
          <ol className="step-item">{moves}</ol>
        </div>
        <div className="game">
          <div className="game-board">
            <Board squares={current.squares}
              onClick={(i) => this.handleClick(i)} />
          </div>
        </div></div>
    )
  }
}
export default Game

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]
  ]
  const len = lines.length
  for (let i = 0; i < len; i++) {
    const [a, b, c] = lines[i]
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a]
    }
  }
  return null
}

// history = [
//   // 第一步之前
//   {
//     squares: [
//       null, null, null,
//       null, null, null,
//       null, null, null,
//     ]
//   },
//   // 第一步之后
//   {
//     squares: [
//       null, null, null,
//       null, 'X', null,
//       null, null, null,
//     ]
//   },
//   // 第二步之后
//   {
//     squares: [
//       null, null, null,
//       null, 'X', null,
//       null, null, 'O',
//     ]
//   },
//   // ...
// ]
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

React 从零开始学习(九)—— 落子有悔 的相关文章

随机推荐

  • ubantu初始化两部曲

    1 配置网络 2 同步windos和ubantu的复制粘贴 sudo apt get install open vm tools sudo apt get install open vm tools desktop
  • gdb调试积累

    1 p打印字符串时不省略 默认情况下 gdb调试时 太长的字符串只显示一部分 如果想要完全显示 可以设置 set print element 0 2 查看产生coredump文件的进程 1 gdb c corefile 使用gdb调试cor
  • git创建分支提示fatal: not a valid object name: ‘master‘解决方案

    文章目录 1 背景描述 2 原因分析 3 解决方案 1 背景描述 在本地使用 git init初始化一个空的git项目后 想使用git branch创建分支时 提示fatal not a valid object name master 2
  • 【我的Java笔记】IO流_输出流中给文本文件追加数据的方法

    1 IO流中给文本追加数据的方法append 1 该方法是Writer类 字符输出流 中的方法 该类为抽象类 可用的子实现类为 OutputStreamWriter和BufferedWriter 2 API中的方法描述 注 字符序列即字符串
  • 基于BP神经网络的Matlab仿真实现

    第一部分 引言 BP神经网络 Back Propagation Neural Network 是一种多层前馈神经网络 主要用于解决非线性问题 它通过反向传播算法进行训练 不断调整网络权重 最终实现输入与输出之间的映射关系 本文将介绍如何使用
  • python处理时间格式:日期、时间、年、月、日、时刻、星期

    原dataframe中的字段timestamp如下 提取其中的日期 时间 年 月 日 时刻 星期 import datetime from datetime import datetime 时间格式转换 获取日期 时间 年 月 日 周几 小
  • 吉比特无源光纤接入用户端设备_网管型光纤收发器产品功能及技术特点详解

    网管型光纤收发器采用主从式管理结构 支持SNMP及Web图形化和Telnet命令行方式带外网管 为电信运营商的维护 管理提供了便捷 可靠的手段 接下来就由飞畅科技小编来为大家介绍下网管收发器的功能及技术特点 一起来看看吧 网管收发器的功能介
  • ubuntu 安装openjdk

    在安装环境的过程中可能需要切换安装版本 安装openjdk sudo apt update sudo apt install openjdk 8 jdk sudo apt install openjdk 11 jdk 切换版本 sudo u
  • linux检查是否有D进程,Linux的CPU-Load虚高之进程的D状态

    写在前面 前几天从同事手里接盘了一个 HHKB 的键盘 虽说是顶级的配置 但是如果不提一句的话估计大家都不会意识到码出这篇博文的工具如此高大上 同时意味着我要持续吃土小半年了 就像之前博文提到的 我工作的重心从业务开发逐渐向基础平台建设转移
  • 模拟cisp-pte 综合题三个key

    1 拿到ip地址 扫端口 扫目录不多说 有1443端口 SQL sever数据库 和27666端口 2 扫出来这个地址 查看一下 访问一下 发现一个是后台 一个存在文件包含的网页 一个大概是上传地址 爆破一下后台发现不成功 试一下利用文件包
  • 虚拟主机的配置

    root localhost nmcli connection modify ens160 ipv4 addresses 192 168 171 137 24 root localhost nmcli connection up ens16
  • 21天Jenkins打卡Day15项目复制

    参考文章 http istester com jenkins 188 html
  • 【visual studio】使用 C++ OpenCV 读取图片失败,数据为空

    这里写自定义目录标题 图片路径问题 图片路径问题 F Documents test image Image BMP 需要改成 F Documents test image Image BMP
  • feign调用第三方接口服务

    前言 做个笔记 下次直接抄 这里需要拿到response的header做验签之类的操作 所以用feign Response来接收响应 正文 第三方接口调用的feign 自测OK import com mea pay common excep
  • 广告案例|10亿数据、查询<10s,论基于OLAP搭建广告系统的正确姿势

    由于流量红利逐渐消退 越来越多的广告企业和从业者开始探索精细化营销的新路径 取代以往的全流量 粗放式的广告轰炸 精细化营销意味着要在数以亿计的人群中优选出那些最具潜力的目标受众 这无疑对提供基础引擎支持的数据仓库能力 提出了极大的技术挑战
  • Google 每天处理约 20000TB 的数据

    Google 热衷于处理全球的信息 每天 他们花费大量时间探索更好的信息整理技术 他们目前使用的技术为 MapReduce 这是一种可以对数据进行并发处理的软件架构 鉴于其简单性与处理大规模数据的能力 MapReduce 是 Google
  • SpringBoot+vue(MyBatis + Shiro + Jwt + Druid + Redis + ElementUI )快速开发框架

    Jeebase 项目介绍 Jeebase是一款前后端分离的开源开发框架 基于springboot vue vue element admin 开发 二期会整合react前端框架Ant Design React 在实际应用中已经使用这套框架开
  • 1188C语言实验——各位数字之和排序

    题目描述 给定n个正整数 根据各位数字之和从小到大进行排序 输入 输入数据有多组 每组数据占一行 每行的第一个数正整数n 表示整数个数 后面接n个正整数 当n为0时 不作任何处理 输入结束 输出 输出每组排序的结果 示例输入 2 1 2 3
  • Using Field in Searching(使用字段搜索)

    Task 1 Use the Fields sidebar to examine search results In the app navigation bar i e the bar towards the top of the bro
  • React 从零开始学习(九)—— 落子有悔

    到目前为止 demo 的操作是不能回退的 点击格子以后 想要记录历史的操作 就需要 使用 slice 函数为每一步创建 squares 数组的副本 同时把这个数组当作不可变对象 这样就可以把所有 squares 数组的历史版本都保存下来了