React 实现井字棋游戏 (tic-tac-toe) 教程 (1) <译自官方文档>
React 实现井字棋游戏 (tic-tac-toe) 教程 (2) <译自官方文档>
React 实现井字棋游戏 (tic-tac-toe) 教程 (3) <译自官方文档>
React 实现井字棋游戏 (tic-tac-toe) 教程 (4) <译自官方文档>
React 实现井字棋游戏 (tic-tac-toe) 教程 (5) <译自官方文档>
KEYS
当你渲染列表中的项目时,React 总会储存各个项目的相关信息。如果你渲染一个有状态 (state) 的组件,React 需要储存状态。不论你如何实现你的组件,React 总会存储对之前状态的引用。
当你更新列表的时候,React 需要判断到底是哪些内容被更新:你可能添加、删除、重排列,或者更新了项目。
比如,从这样:
code
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
变成这样:
code
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
在人眼看来,这只不过就是把 Alexa 和 Ben 调换了下位置,又加上了 Claudia。但 React 只是个程序,它不懂你想要怎么干。因而, React 要求,必须为列表中的每个元素都指定一个 key 属性,即一个字符串,用来区分各个组件。在本案例中,alexa
,ben
,claudia
就可以是很合适的 key。如果项目对应于数据库中的对象,那数据库 ID 通常也是一个好的选择:
code
<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>
key
是 React 保留的特殊属性(和ref
一样,那个更高级)。当元素被创建,React 拉取key
属性,并将其直接储存到返回的元素上。尽管它看起来像是 props 的一部分,但其实并不能通过this.props.key
来引用。在判断哪个子组件应该被更新时,React 自动使用 key。组件自己是没办法查询自己的 key 的。
当列表被重新渲染,React 会在新的版本提取每个元素,寻找里面有没有能和之前列表相匹配上的 key。当一个 key 被添加到集合中时,一个组件实例会被创建;当一个 key 被删除时,一个组件实例会被销毁。React 通过 key 来识别每个组件的身份,组件由此得以在重新渲染的过程中保持状态(state)。如果改变了组件的 key,则该组件实例将被销毁,再重创建一个新的组件实例。这样,原来的状态无法继承,而是创建新的状态。
我们强烈建议,只要你建立动态列表,你应该设置合适的 key。如果你没有合适的 key,或许你该考虑一下重构你的数据,来得到合适的 key。
如果你没有指定任何 key,React 将会发出警告,并回头使用数组的 index 作为 key。但这么干也是不对的,因为当你重新排列表单中的元素,或者 增/删 非列表底部的项目时,就会出问题。明确地传入key={i}
虽然会让警告消失,但还是存在相同的问题。所以,大多数情况下,我们也不推荐这么做。
组件的 key 不需要再全局环境下保持唯一,只需要在兄弟组件间保持唯一就可以了。
实现穿越功能
记录历史步骤的列表里头,每一个步骤都有了一个唯一的 ID,即这一步走的时候。在 Game 组件的render
方法里,这么添加 key:<li key={move}>
,刚才的警告就会消失。
code
const moves = history.map((step, move) => {
const desc = move ?
'Move #' + move :
'Game start';
return (
<li key={move}>
<a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
</li>
);
});
点击里面的步骤,会报错。因为jumpTo
方法还没定义。我们在 Game 组件的 state 里面新添加一条,用来指示当前的我们正在查看的步骤。
首先,在 Game 组件的constructor
中,添加初始状态:stepNumber: 0
。
code
class Game extends React.Component {
constructor() {
super();
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
xIsNext: true,
};
}
下一步,在 Game 组件中,定义jumpTo
方法,用以更新那条状态。xIsNext
同样需要更新,如果一步棋的序号数是偶数,我们就把xIsNext
的值设为 true。
向Game的类增加jumpTo
方法:
code
handleClick(i) {
// 本方法不做改动
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) ? false : true,
});
}
render() {
// 本方法不做改动
}
为了实现在新走一步棋时,stepNumber
可以更新的功能,我们在 Game 组件handleClick
中的状态更新语句里添加stepNumber: history.length
。
code
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.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
});
}
现在,你就可以修改 Game 组件的render
函数,以实现从历史记录中读取步骤了。
code
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
// the rest has not changed
现在你再点击列表里的步骤,棋盘应该就能立即更新,穿越回当时的那一步了。
你可能还想要更新handleClick
,以便在读取当前 board 状态的时候,获取stepNumber
。这样就能在穿越回去后,又点击棋盘时,创建新的步骤记录。(提示:最简单的办法,就是在handleClick
的一开始,用.slice()
把历史记录额外的元素切下来。)
圆满完成
现在,你的井字棋已经实现了如下功能:
* 你可以玩井字棋游戏;
* 当有玩家获胜时,宣布结果;
* 存储棋局的历史步骤记录;
* 允许玩家穿越回之前,查看当时棋盘的格局。
干得漂亮!我们希望你已经觉得自己对 React 有了较为深入的把握。
如果你还有时间,想要练习新学到的技能,这里列出了一些难度提升的改进:
- 记录落子的位置时,以“(1,3)”的格式显示,而不仅仅只显示“6”;
- 在历史步骤记录表单中,对当前选中的步骤加粗显示;
- 重写 Board 组件,使用两个循环来构造小方格,而非直接写死(hardcode)。
- 添加切换按钮,实现步骤排列的升序排列或降序排列。
- 有人胜出的时候,将那一排胜利的小方格高亮显示。
通过本教程,我们接触了一些 React 的概念,包括:元素 elements, 组件 components, props 和 状态 state。想要进一步深入了解这些话题,请查看其它文档。想要进一步学习如何定义组件,点击React.Component
API引用文档。
译者注
这个仓库是我实现的tic-tac-toe,在文档基础上添加了一些扩展功能:
- 增加和棋判断;
- 以直角坐标系的形式(x,y)记录落子位置;
- 高亮显示胜负手,使结局一目了然;
- 实现了通过按钮,切换步骤历史记录的正序、逆序排列;
- 添加了重置按钮,一键重新开始;
- 高亮显示历史记录列表中的当前选中项;
- 添加了 AI 功能,可人机对弈,亦可赛艇帮人决策。