跳到主要内容React 实战:从零构建井字棋游戏 | 极客日志JavaScript大前端
React 实战:从零构建井字棋游戏
React 组件化开发实战,通过井字棋游戏演示状态管理、Props 传递及事件处理核心机制。涵盖静态页面构建、交互逻辑设计、不可变数据原则及胜负判定算法,帮助开发者深入理解 React 渲染原理与最佳实践。
山野来信2 浏览 React 实战:从零构建井字棋游戏
跟着官方教程一步步来,我们不仅能学会 React 的基础语法,更能理解组件化设计的核心思想。这次我们通过一个经典的井字棋(Tic-Tac-Toe)项目,串联起组件创建、状态管理、Props 传递以及事件处理等关键知识点。
核心概念回顾
在开始写代码前,先理清几个基础点:
- 组件是 UI 的独立单元,可以是单个标签或整个页面。
- JSX 让 JavaScript 和 HTML 结构融合在一起。
- 组件命名必须以大写字母开头,区别于 HTML 标签的小写。
- Props 用于父组件向子组件传递数据。
- State 用于组件内部维护可变数据。
第一步:构建静态页面
我们先搭建一个 3x3 的棋盘骨架。这里需要两个组件:Square 表示单个格子,Board 负责组合九个 Square。
function Square({ value }) {
return <button className="square">{value}</button>;
}
export default function Board() {
return (
<>
<div className="board-row">
<Square value={null} />
<Square value={null} />
<Square value={null} />
</div>
<div className="board-row">
);
}
<Square value={null} />
<Square value={null} />
<Square value={null} />
</div>
<div className="board-row">
<Square value={null} />
<Square value={null} />
<Square value={null} />
</div>
</>
此时页面是空的,但结构已经就绪。接下来我们要让它动起来。
第二步:添加交互与本地状态
当用户点击格子时,我们希望它显示 "X"。这需要给 Square 组件添加状态。
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button className="square" onClick={handleClick}>
{value}
</button>
);
}
注意这里的 onClick={handleClick}。不要写成 onClick={handleClick()},否则函数会在渲染时立即执行,而不是等待点击。React 会把你传递的函数引用保存起来,等到事件触发时才调用。
每个 Square 都有自己的 state,互不干扰。但现在的逻辑有个问题:如果我想让 Board 知道哪个格子被点了怎么办?这时候就需要 Props 了。
第三步:提升状态 (Lifting State Up)
为了判断胜负,Board 需要知道所有格子的状态。我们把 squares 数组的状态提升到 Board 组件中,然后通过 Props 传给 Square。
同时,我们需要把更新状态的函数也传下去,这样 Square 点击时能通知 Board。
import { useState } from 'react';
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
nextSquares[i] = 'X';
setSquares(nextSquares);
}
return (
<>
<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[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}
关键点: 在 JSX 中传递事件处理函数时,应该传递函数引用或返回函数的函数(如箭头函数),而不是直接调用结果。直接调用会导致无限循环渲染。
第四步:完善游戏逻辑
现在我们需要支持交替落子(X 和 O),并判断胜负。
- 交替落子:增加
xIsNext 状态来控制当前轮到谁。
- 不可变性:修改数组时不要直接赋值,而是使用
slice() 创建副本,这是 React 检测状态变化的重要原则。
- 胜负判定:编写辅助函数检查三行、三列及对角线。
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;
}
最后,整合所有逻辑,加上状态提示(当前玩家或获胜者)。
import { useState } from 'react';
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
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();
nextSquares[i] = xIsNext ? 'X' : 'O';
setSquares(nextSquares);
setXIsNext(!xIsNext);
}
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">
handleClick(6)} />
handleClick(7)} />
handleClick(8)} />
);
}
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 井字棋就完成了。通过这个项目,你应该对组件的生命周期、状态提升以及不可变数据有了更直观的感受。后续可以在此基础上扩展历史记录、撤销功能等进阶特性。
<Square value={squares[6]} onSquareClick={() =>
<Square value={squares[7]} onSquareClick={() =>
<Square value={squares[8]} onSquareClick={() =>
</div>
</>
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online