React 02
1 React 组件的导出以及组件结构的组织
这段内容主要是关于 React 组件的导出以及组件结构的组织,我们逐步来解释:
1. export default
的作用
在 React 中,export default
是用来指定模块的默认导出的。一个模块(通常是一个文件)只能有一个默认导出。当其他文件引入这个模块时,默认导出的内容会被作为该模块的主要内容引入。
2. 组件导出的调整
- 之前可能
Board
组件是用export default
导出的,也就是:
这时候,export default function Board() {//... }
index.js
(或者其他引入该组件的文件)会把Board
作为顶层组件来使用。 - 现在需要把
Game
组件作为顶层组件,所以要:- 移除
Board
组件前面的export default
。 - 给
Game
组件添加export default
,也就是:export default function Game() {//... }
这样,
index.js
就会使用Game
组件作为顶层组件,而不是Board
组件了。 - 移除
3. Game
组件的结构
export default function Game() {return (<div className="game"><div className="game-board"><Board /></div><div className="game-info"><ol>{/*TODO*/}</ol></div></div>);
}
- 最外层的
div
类名为game
,作为整个游戏组件的容器。 - 里面的
div
类名为game-board
,用来包裹Board
组件,Board
组件负责渲染棋盘部分。 - 另一个
div
类名为game-info
,里面的<ol>
标签目前是一个待办(TODO
),后续会用来展示游戏相关的信息,比如下棋的步骤、当前玩家、游戏结果等。
4. 总结
这段内容的核心是调整组件的默认导出,让 Game
成为顶层组件,同时 Game
组件的结构为后续添加游戏信息(如下棋历史、胜负判断等)预留了空间,使整个井字棋应用的结构更加清晰,便于后续功能的扩展。
2 状态管理与数组展开语法
这段代码和说明是关于 React(或类似前端框架中)状态管理与数组展开语法的应用,核心是理解如何通过数组展开语法来维护 “游戏步骤历史” 这类序列型状态。
1. 代码结构与核心逻辑
代码定义了一个 Game
组件(React 组件常见形式),内部有 handlePlay
函数,用于 ** 更新 “游戏历史” 和 “当前玩家(X/O 轮次)”** 这两个状态:
setHistory([...history, nextSquares])
:更新 “游戏步骤历史” 的状态。setXIsNext(!xIsNext)
:切换当前玩家(比如从 X 轮到 O 轮,或反之)。
2. 数组展开语法(...history
)的作用
...
是数组展开语法(也叫 “扩展运算符”),作用是:把原数组 history
里的所有元素 “拆出来”,逐个放到新数组里。
结合代码,[...history, nextSquares]
的效果是:
- 先把
history
中已有的所有数组元素(每个元素代表某一步的游戏局面)依次放入新数组; - 再把
nextSquares
(代表 “下一步的游戏局面”)放到新数组的最后。
这样就得到了一个包含所有历史步骤 + 新步骤的 “完整历史数组”。
3. 示例理解(最关键的部分)
官方给的示例非常直观:
- 假设
history
原本是[[null,null,null], ["X",null,null]]
:- 第一个元素
[null,null,null]
:代表 “游戏初始状态(所有格子都是空的)”; - 第二个元素
["X",null,null]
:代表 “第一步,X 下在第一个格子,其余为空”。
- 第一个元素
- 假设
nextSquares
是["X",null,"O"]
:代表 “第二步,O 下在第三个格子,此时局面是 X 在第一格、O 在第三格,中间为空”。
那么 [...history, nextSquares]
生成的新数组就是:[[null,null,null], ["X",null,null], ["X",null,"O"]]
—— 把 “初始状态”“第一步”“第二步” 这三个步骤,按顺序整合到了新的历史数组里。
4. 为什么要这么做?
在前端框架(如 React)中,状态是不可直接修改的(要通过 setState
这类方法更新)。
如果直接对 history
数组 push
新元素,属于 “修改原数组”,不符合框架的 “不可变状态” 设计思想。
而用 [...history, nextSquares]
,是创建了一个全新的数组(包含历史 + 新步骤),再通过 setHistory
把 “新数组” 设为新状态 —— 既维护了 “游戏步骤按顺序记录” 的逻辑,又符合 “状态不可变” 的要求。
总结:这段代码通过数组展开语法,优雅地实现了 “游戏步骤历史的追加式更新”,保证每一步都被有序记录,同时遵循前端框架的状态管理规范。
3 React 中 key
属性的特点
key 是 React 中一个特殊的保留属性。创建元素时,React 提取 key 属性并将 key 直接存储在返回的元素上。尽管 key 看起来像是作为 props 传递的,但 React 会自动使用 key 来决定要更新哪些组件。组件无法询问其父组件指定的 key。
key
作为 React 内部用于优化渲染性能的特殊属性,有几个关键特性需要明确:
-
唯一性与作用域:
key
在兄弟节点之间必须唯一(无需全局唯一),React 通过它识别元素的身份,判断是复用还是重新创建组件。 -
非 props 特性:尽管写法上类似 props(如
<Item key={id} />
),但子组件无法通过this.props.key
或props.key
获取key
的值,它完全由 React 内部管理。 -
渲染优化核心:当列表数据变化时,React 会对比新旧节点的
key
:- 若
key
相同,尝试复用原有组件并更新内容; - 若
key
不同,则销毁旧组件并创建新组件。
- 若
-
避免使用索引作为 key:在列表项可能重排、增删的场景中,使用索引作为
key
会导致 React 误判元素身份,反而影响性能或引发状态错乱,推荐使用数据自身的唯一标识(如 ID)。
理解 key
的作用机制,有助于更高效地编写 React 列表渲染逻辑,避免因 key
使用不当导致的性能问题或异常行为。
------------
实战一:井子棋源代码
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 [history, setHistory] = useState([Array(9).fill(null)]);const [currentMove, setCurrentMove] = useState(0);const xIsNext = currentMove % 2 === 0;const currentSquares = history[currentMove];function handlePlay(nextSquares) {const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];setHistory(nextHistory);setCurrentMove(nextHistory.length - 1);}function jumpTo(nextMove) {setCurrentMove(nextMove);}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;
}