当前位置: 首页 > news >正文

快速学习React-(第二天)-完成井字棋游戏

存储落子历史

如果你改变了 squares 数组,实现时间旅行将非常困难。

但是,你在每次落子后都使用 slice() 创建 squares 数组的新副本,并将其视为不可变的。这将允许你存储 squares 数组的每个过去的版本,并在已经发生的轮次之间“来回”。

把过去的 squares 数组存储在另一个名为 history 的数组中,把它存储为一个新的 state 变量。history 数组表示所有棋盘的 state,从第一步到最后一步,其形状如下:

[// Before first move[null, null, null, null, null, null, null, null, null],// After first move[null, null, null, null, 'X', null, null, null, null],// After second move[null, null, null, null, 'X', null, null, null, 'O'],// ...]

再一次“状态提升”

你现在将编写一个名为 Game 的新顶级组件来显示过去的着法列表。这就是放置包含整个游戏历史的 history state 的地方。

history state 放入 Game ,这使 Game 组件可以完全控制 Board 的数据,并使它让 Board 渲染来自 history 的之前的回合。

首先,添加一个带有 export defaultGame 组件。让它渲染 Board 组件和一些标签。

要删除 function Board() { 声明之前的 export default 关键字,并将它们添加到 function Game() { 声明之前。这会告诉你的 index.js 文件使用 Game 组件而不是你的 Board 组件作为顶层组件。

function Board() {// ...
}export default function Game() {return (<div className="game"><div className="game-board"><Board /></div><div className="game-info"><ol>{/*TODO*/}</ol></div></div>);
}

Game 组件添加一些 state 以跟踪下一个玩家和落子历史:

export default function Game() {const [xIsNext, setXIsNext] = useState(true);const [history, setHistory] = useState([Array(9).fill(null)]);// ...

要渲染当前落子的方块,你需要从 history 中读取最后一个 squares 数组。你不需要 useState——你已经有足够的信息可以在渲染过程中计算它:

export default function Game() {const [xIsNext, setXIsNext] = useState(true);const [history, setHistory] = useState([Array(9).fill(null)]);const currentSquares = history[history.length - 1];

接下来,在 Game 组件中创建一个 handlePlay 函数,Board 组件将调用该函数来更新游戏。将 xIsNextcurrentSquareshandlePlay 作为 props 传递给 Board 组件:

export default function Game() {const [xIsNext, setXIsNext] = useState(true);const [history, setHistory] = useState([Array(9).fill(null)]);const currentSquares = history[history.length - 1];function handlePlay(nextSquares) {// TODO}return (<div className="game"><div className="game-board"><Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />//...)
}

现在,删除调用 useStateBoard 函数的前两行将 Board 组件里面的 handleClick 中的 setSquaressetXIsNext 调用替换为对新 onPlay 函数的一次调用,这样 Game 组件就可以在用户单击方块时更新 Board

export default function Board() {const [xIsNext, setXIsNext] = useState(true);const [squares, setSquares] = useState(Array(9).fill(null));function handleClick(i) {if (calculateWinner(squares) || squares[i]) {return;}const nextSquares = squares.slice();if (xIsNext) {nextSquares[i] = 'X';} else {nextSquares[i] = 'O';}setSquares(nextSquares);setXIsNext(!xIsNext);}......
}
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);}//...
}

Board 组件完全由 Game 组件传递给它的 props 控制。你需要在 Game 组件中实现 handlePlay 函数才能使游戏重新运行。

handlePlay 被调用应该做什么?请记住,Board 以前使用更新后的数组调用 setSquares;现在它将更新后的 squares 数组传递给 onPlay

handlePlay 函数需要更新 Game 的 state 以触发重新渲染,但是你没有可以再调用的 setSquares 函数——你现在正在使用 history state 变量来存储这些信息。你需要追加更新后的 squares 数组来更新 history 作为新的历史入口。你还需要切换 xIsNext,就像 Board 过去所做的那样:

export default function Game() {//...function handlePlay(nextSquares) {setHistory([...history, nextSquares]);setXIsNext(!xIsNext);}//...
}

在这里,[...history, nextSquares] 创建了一个新数组,其中包含 history 中的所有元素,后跟 nextSquares。(你可以将 ...history 展开语法理解为“枚举 history 中的所有元素”。)

例如,如果 history[[null,null,null], ["X",null,null]]nextSquares["X",null,"O"],那么新的 [...history, nextSquares] 数组就是 [[null,null,null], ["X",null,null], ["X",null,"O"]]

此时,你已将 state 移至 Game 组件中,并且 UI 应该完全正常工作,就像重构之前一样。这是此时代码的样子:

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 [xIsNext, setXIsNext] = useState(true);const [history, setHistory] = useState([Array(9).fill(null)]);const currentSquares = history[history.length - 1];function handlePlay(nextSquares) {setHistory([...history, nextSquares]);setXIsNext(!xIsNext);}return (<div className="game"><div className="game-board"><Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /></div><div className="game-info"><ol>{/*TODO*/}</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;
}

选择Key

你需要为每个列表项指定一个 key 属性,以将每个列表项与其兄弟项区分开来。如果你的数据来自数据库,数据库 ID 可以用作 key:

<li key={user.id}>{user.name}: {user.taskCount} tasks left
</li>
  1. key 的作用:作为列表项的“身份标识”,帮助 React 在重新渲染时识别每个列表项:
    • 新 key 出现 → 创建新组件;
    • 旧 key 消失 → 销毁对应组件;
    • key 匹配 → 复用原有组件(保持状态)。
  1. key 与状态的关系:key 决定组件是否复用:
    • key 不变 → 组件复用,状态保留;
    • key 变化 → 组件销毁重建,状态重置。
  1. key 的特殊性
    • 是 React 内部使用的保留属性,不算作子组件的 props,子组件无法获取;
    • 只需在同级列表项中唯一,无需全局唯一。
  1. 使用规范
    • 动态列表必须指定合适的 key(优先用数据自带的唯一标识,如 user.id);
    • 禁止用数组索引作 key(会在增删排序时出问题),即使显式传 key={i} 也不推荐;
    • 若没有合适的 key,应调整数据结构以提供唯一标识。

实现时间旅行

在井字棋游戏的历史中,过去的每一步都有一个唯一的 ID 与之相关联:它是动作的序号。落子永远不会被重新排序、删除或从中间插入,因此使用落子的索引作为 key 是安全的。

Game 函数中,你可以将 key 添加为 <li key={move}>,如果你重新加载渲染的游戏,React 的“key”错误应该会消失:

import { useState } from 'react';// TODO}//histroy是一个数组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;
}

在你可以实现 jumpTo 之前,你需要 Game 组件来跟踪用户当前正在查看的步骤。为此,定义一个名为 currentMove 的新 state 变量,默认为 0

export default function Game() {const [xIsNext, setXIsNext] = useState(true);const [history, setHistory] = useState([Array(9).fill(null)]);const [currentMove, setCurrentMove] = useState(0);const currentSquares = history[history.length - 1];//...
}

接下来,更新 Game 中的 jumpTo 函数来更新 currentMove。如果你将 currentMove 更改为偶数,你还将设置 xIsNexttrue

export default function Game() {// ...function jumpTo(nextMove) {setCurrentMove(nextMove);setXIsNext(nextMove % 2 === 0);}//...
}

你现在将对 GamehandlePlay 函数进行两处更改,该函数在你单击方块时调用。

  • 如果你“回到过去”然后从那一点开始采取新的行动,你只想保持那一点的历史。不是在 history 中的所有项目(... 扩展语法)之后添加 nextSquares,而是在 history.slice(0, currentMove + 1) 中的所有项目之后添加它,这样你就只保留旧历史的那部分。
  • 每次落子时,你都需要更新 currentMove 以指向最新的历史条目。
function handlePlay(nextSquares) {const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];setHistory(nextHistory);setCurrentMove(nextHistory.length - 1);setXIsNext(!xIsNext);
}

最后,你将修改 Game 组件以渲染当前选定的着法,而不是始终渲染最后的着法:

export default function Game() {const [xIsNext, setXIsNext] = useState(true);const [history, setHistory] = useState([Array(9).fill(null)]);const [currentMove, setCurrentMove] = useState(0);const currentSquares = history[currentMove];//修改了这里 而不是 = history[history.length - 1];}

http://www.dtcms.com/a/535842.html

相关文章:

  • 石家庄网站开发培训家教网站开发公司
  • 如何制作网址快捷方式深圳网站优化怎么做
  • 聊聊Spark的分区
  • 国产之光:奥威BI的AI数据分析平台,重新定义商业智能
  • Android ContentProvier
  • uni-app OCR图文识别
  • 二叉树的多种遍历方式
  • Vue3 + Electron + Node.js 桌面项目完整开发指南
  • 【Node.js】Node.js 模块系统
  • 古籍影文公开古籍OCR检测数据集VOC格式共计8个文件
  • 网站的对比哪些网站是做免费推广的
  • 网站建设的整体流程有哪些?建筑工程网站建站方案
  • 区块链的密码学基石:沙米尔秘密共享(SSS)数学原理详解
  • 单例模式详解:从基础到高级的八种实现方式
  • 改版网站收费wordpress国人主题
  • web3.0是什么
  • 计网:网络层
  • git学习3
  • HarmonyOS图形图像处理与OpenGL ES实战
  • SunX:以合规正品,重塑Web3交易信任
  • nacos 使用oceanbase(oracle模式)作为数据源
  • 网站排名优化策划网站一个人可以做吗
  • 基于springboot的民宿在线预定平台开发与设计
  • 脚本探索--Spatial HD进行CNV分析
  • 介绍一下Hystrix的“舱壁模式”和“熔断状态机”
  • 基数排序(Radix Sort)算法简介
  • 【C++项目】基于设计模式的同步异步日志系统(前置基础知识)
  • JDK8时间相关类,时间对象都是不可变的
  • Java内存模型(JMM)与JVM内存模型
  • h5响应式网站模板如何做公司自己的网站首页