入门教程:简介 React
本教程适用于 React 初学者。
在我们开始之前
我们将在本教程中构建一个小游戏。你可能想跳过本教程,因为你没有建立游戏 - 但给它一个机会。 您将在本教程中学习的技术是构建任何 React 应用程序的基础, 掌握本教程将使您对 React 有深入的了解。
提示:
本教程专为喜欢边学边做的人而设计。 如果您更喜欢从头开始学习概念,请查看我们的 一步一步学习指南。 您可能会发现本教程和指南是相互补充。
本教程分为几个部分:
- 教程设置 将为您提供 一个教程的起点 。
- 概述 将教你 React 的基本原理:组件(components),属性(props)和状态(state)。
- 完成游戏 将教你 React开发中最常用的技巧。
- 添加过程回放 将让您更深入地了解 React 的独特优势。
您不必一次完成所有部分以获得本教程的精髓。试着尽你所能学习 - 即使是一节或两节。
您可以按照教程中的说法复制和粘贴代码, 但我们建议手工输入。 这将有助于您加深记忆和更强的理解力。
我们正在构建什么
在本教程中,我们将构建一个交互式的 tic-tac-toe(井字棋) 游戏。
如果您愿意,可以在这里查看最终结果:最终结果。如果你对不理解代码,或者你对语法不熟悉,不要担心。我们将在本教程中逐步学习如何构建这个游戏。
我们建议您在继续本教程之前玩一下井字游戏。 您会注意到的一个功能是游戏棋盘右侧有一个编号列表。 此列表为您提供游戏中发生的所有动作的历史记录,并随着游戏的进行而更新。
一旦熟悉它,你就可以关闭井字游戏。 我们将从本教程中的一个更简单的模板开始。 我们的下一步是为您安排,以便您可以开始构建游戏。
预备知识(Prerequisites)
我们需要熟悉 HTML 和 JavaScript ,但是即使你以前没有使用过它们,也应该能够跟随本教程完成这个示例。我们还假设您已经熟悉了编程概念,如函数,对象,数组,以及一定程度的类。
如果您需要刷新你的 JavaScript 知识,我们建议您阅读本指南。
请注意,我们还使用了 ES6 的一些功能,最近版本的 JavaScript 。
在本教程中,我们使用的是箭头函数,classes,let
,和 const
语句。
您可以使用 Babel REPL 来检查ES6代码编译的内容。
教程的设置
有两种方法可以完成本教程:你可以直接在浏览器中编写代码,或者你可以在你的机器上建立一个本地开发环境。你可以根据自己感觉舒适的方式选择其中一个选项。
设置选项1:在浏览器中编写代码
这是上手最快的方法!
我们将在本指南中使用一个名为 CodePen 的在线编辑器。您可以先打开此 开始代码 。它应该显示一个空的 tic-tac-toe 字段。我们将在本教程中编辑该代码。
你现在可以跳过选项2,关于设置本地开发环境的介绍,并直接转到 概述。
设置选项2:本地开发环境
这完全是可选的,对于本教程来说不是必须的!
可选:使用首选文本编辑器在本地按照本教程设置项目
此设置需要更多的前期工作,但允许您使用您选择的编辑器完成教程。 以下是要遵循的步骤:
- 确保您安装了最新版本的 Node.js 。
- 按照 Create React App 安装说明 创建一个新项目。
npm install -g create-react-app
create-react-app my-app
- 删除新项目
src/
文件夹中的所有文件(不要删除文件夹,只删除里面的文件)。
cd my-app
rm -f src/*
-
在
src/
文件夹中添加一个名为index.css
的文件,这个文件中包含的 CSS代码 。 -
在
src/
文件夹中添加一个名为index.js
的文件,这个文件中包含的 JS代码 。 -
然后在
src/
文件夹index.js
的文件中,在其顶部添加以下 3 行代码:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
现在,如果您在项目文件夹中运行 npm start
,并在浏览器中打开 http://localhost:3000
,你应该看到一个空的 tic-tac-toe 字段。
我们建议你按照 这些说明 为你的编辑器配置设置语法高亮。
求助,我被卡住了!(Help, I’m Stuck!)
如果你被卡住了, 查看 社区支持资源。特别是,Reactiflux 聊天室 是获得快速帮助的好方法。如果您在任何地方找不到好的答案,请提出 issue ,我们将帮助你。
概述(Overview)
现在您已经设置好了,让我们来看看React!
什么是 React ?(What is React?)
React是一个声明式的,高效的,并且灵活的用于构建用户界面的 JavaScript 库。它允许您使用”components(组件)“(小巧而独立的代码片段)组合出各种复杂的UI。
React有几种不同的组件,但是我们将从 React.Component
子类开始:
class ShoppingList extends React.Component {
render() {
return (
<div className="shopping-list">
<h1>Shopping List for {this.props.name}</h1>
<ul>
<li>Instagram</li>
<li>WhatsApp</li>
<li>Oculus</li>
</ul>
</div>
);
}
}
// Example usage: <ShoppingList name="Mark" />
我们能在一秒钟内理解有趣的类 XML 的标签。你的组件告诉 React 你要呈现的内容 - 然后,当数据更改时,React 将有效地更新并渲染正确的组件。
在这里,ShoppingList 是一个 React 组件类 ,或 React 组件类型。组件接收参数,称为 props
(属性),并通过 render
方法返回一个显示的视图层次结构。
render
方法返回您要渲染的内容描述,然后 React 接受该描述并将其渲染到屏幕上。特别是,render
返回一个 React 元素,这是一个渲染内容的轻量级描述。大多数 React 开发人员使用一种名为 JSX 的特殊语法,可以更容易地编写这些结构。<div />
语法在构建时被转换为 React.createElement('div')
。上面的例子等价于:
return React.createElement('div', {className: 'shopping-list'},
React.createElement('h1', /* ... h1 children ... */),
React.createElement('ul', /* ... ul children ... */)
);
如果你好奇,createElement()
在 API参考 中有更详细的描述,但是我们不会在本教程中直接使用它。相反,我们将继续使用 JSX 。
您可以将任何 JavaScript 表达式放在 JSX 中的大括号内。每个 React 元素都是一个真正的 JavaScript 对象,您可以将其存储在变量中或传递给程序。
ShoppingList
组件仅渲染内置的 DOM 组件,但你可以通过使用 <ShoppingList />
轻松地编写自定义的 React 组件。每个组件都是封装的,因此它可以独立操作,这允许你从简单的组件构建复杂的UI。
查看入门代码
从这个例子开始:起始代码 。
它包含了我们今天正在构建的应用外壳。 我们提供了风格,所以您只需要担心 JavaScript 。
特别是我们有三个组成部分:
- Square(方格)
- Board(棋盘)
- Game(游戏)
Square(方格) 组件渲染为一个单独的 <button>
, Board(棋盘) 会渲染为 9 个 Square(方格) ,而 Game(游戏) 组件会渲染为一个 Board(棋盘),还有一些占位符,我们稍后会填充。到目前为止,没有一个组件是交互的。
通过 Props 传递数据(Passing Data Through Props)
初次尝试,让我们试试从 Board(棋盘) 组件传递一些数据到 Square(方格) 组件。
在 Board(棋盘) 组件的 renderSquare
方法中,更改代码以将 value
prop(属性) 传递给 Square(方格) 组件:
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />;
}
然后修改 Square(方格) 的render
方法,通过使用 {this.props.value}
替换 {/* TODO */}
来显示该值:
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value}
</button>
);
}
}
之前:
之后: 您应该可以在渲染输出中的每个正方形中看到一个数字。
查看当前代码 。
制作交互式组件
当您点击 Square(方格) 组件 时,填入 “X”。 尝试将 Square(方格) 的 render()
函数中返回的按钮标签更改为类似这个样子:
class Square extends React.Component {
render() {
return (
<button className="square" onClick={() => alert('click')}>
{this.props.value}
</button>
);
}
}
如果现在点击一个正方形,你应该在浏览器中会看到一个 alert 信息。
注意:
为了节省代码并避免
this
的混乱行为,我们将在这里和下面的事件处理程序中使用 箭头函数语法 :class Square extends React.Component { render() { return ( <button className="square" onClick={() => alert('click')}> {this.props.value} </button> ); } }
请注意如何使用
onClick={() => alert('click')}
,我们将传递 一个函数 作为onClick
prop(属性)。它只在点击后触发。忘记() =>
并直接编写onClick={alert('click')}
是一个常见错误,并且每次组件重新渲染时都会触发 alert 。
下一步,我们希望 Square(方格) 组件 “记住” 它被点击,并标记为 “X” 。 为了 “记住” 这些事情,组件使用 state(状态) 。
React 组件可以通过在构造函数中设置 this.state
来拥有 state(状态) ,构造函数应该被认为是组件的私有。 让我们在 Square(方格) 组件的 state(状态) 中存储当前值,并在单击方格时更改它。
首先,在类中添加一个构造函数来初始化 state(状态):
class Square extends React.Component {
constructor() {
super();
this.state = {
value: null,
};
}
render() {
return (
<button className="square" onClick={() => alert('click')}>
{this.props.value}
</button>
);
}
}
注意:
在 JavaScript classes(类)中, 在定义子类的构造函数时,你需要始终调用
super
。所有具有constructor
的 React 组件类都应该以super(props)
调用启动它。
现在更改 Square(方格) 组件的 render
方法以显示当前 state(状态) 的值,并在点击时切换:
- 将
<button>
标签中的this.props.value
替换为this.state.value
。 - 用
() => this.setState({value: 'X'})
替换事件处理程序中的() => alert()
。 - 将
className
和onClick
道具放在不同的行上以提高可读性。
在这些更改之后, Square(方格) 的render
方法返回的 <button>
标记如下所示:
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
<button
className="square"
onClick={() => this.setState({value: 'X'})}
>
{this.state.value}
</button>
);
}
}
通过 Square(方格) 的 render
方法中的 onClick
处理程序调用this.setState
,
只要单击 <button>
,我们就告诉 React 重新渲染 Square(方格) 。
更新后, Square(方格) 的 this.state.value
的值将是 'X'
,
所以我们会在游戏棋盘上看到X
。如果你点击任何 Square(方格),就会出现一个X
。
当你在一个组件中调用 setState
时,
React 也会自动更新其子组件。
开发者工具(Developer Tools)
Chrome 和 Firefox 的 React Devtools extension 可让您在浏览器的开发者工具中检查 React 组件树。
React DevTools 允许您检查任何组件中的 props(属性) 和 state(状态) 。
安装完成后,您可以右键单击页面上的任何元素,点击 “检查(Inspect)” 打开开发者工具, “React” 选项卡将显示在最右侧。
但是,请注意,在 CodePen 中使用需要一些额外的步骤:
- 登录 或 注册并确认你的电子邮件(防止垃圾邮件)。
- 点击 “Fork” 按钮。
- 单击 “Change View(更改视图)” ,然后选择 “Debug mode(调试模式)”。
- 在新 tab 页中打开的,现在 开发者工具 应该有一个 React 选项卡。
完成游戏
我们现在拥有了井字游戏的基本构建模块。 要完成一个完整的游戏,我们现在需要在棋盘上交替放置“X”和“O”, 我们需要一种方法来确定胜利者。
State(状态) 提升
目前,每个 Square(方格) 组件都维持游戏的状态。为了检查获胜者,我们将在一个地方保存这 9 个 Square(方格) 的状态值。
你可能会认为 Board(棋盘) 应该可以获取到每个 Square(方格) 的当前 state(状态) 。尽管在 React 中这样做在技术上是可行的,但是我们不鼓励这么做,因为它往往使代码难以理解,更脆弱,更难重构。相反,最好的解决办法是将 state(状态) 存储在 Board(棋盘) 组件中,而不是在每个 Square(方格) 组件中 。 Board(棋盘) 组件可以通过传递props(属性)来告诉每个 Square(方格) 组件要显示什么,就像我们将数字索引传递给每个Square(方格) 时所做的那样。
要从多个子级收集数据 或 使两个子组件之间相互通信,您需要在其父组件中声明共享 state(状态) 。父组件可以使用props(属性) 将 state(状态) 传递回子节点;这可以使子组件彼此同步并与父组件保持同步。
当重构 React 组件时,提升 state(状态) 是非常常见的,所以让我们借此机会尝试一下。在 Board(棋盘) 组件中添加一个构造函数,并设置其初始 state(状态) 为包含一个具有 9 个空值的数组,对应 9 个方格:
class Board extends React.Component {
constructor() {
super();
this.state = {
squares: Array(9).fill(null),
};
}
renderSquare(i) {
return <Square value={i} />;
}
render() {
return (
<div>
<div className="status">{status}</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>
);
}
}
我们稍后会填充,以使棋盘看起来像这样:
[
'O', null, 'X',
'X', 'X', 'O',
'O', null, null,
]
Board(棋盘) 组件的 renderSquare
方法目前看起来像这样:
renderSquare(i) {
return <Square value={i} />;
}
在一开始的时候,我们从 Board(棋盘) 传递 value
prop(属性) 来显示每个 Square(方格) 中 0 到 8 的数字。
与前面的步骤不同,这次我们用由Square(方格)自己的状态确定的 “X” 标记替换数字。
这就是为什么 Square(方格) 目前忽略了从 Board(棋盘) 传递给它的 value
prop(属性)。
我们现在再次使用 prop(属性) 传递机制。
我们将修改 Board(棋盘) ,以指示每个 Square(方格) 的当前值(“X”,“O’”或“null”)。
我们已经在 Board(棋盘) 的构造函数中定义了 squares
数组,
我们将修改 Board(棋盘) 的 renderSquare
方法来读取它:
renderSquare(i) {
return <Square value={this.state.squares[i]} />;
}
每个 Square(方格) 现在都会收到一个 value
prop(属性),它要么是 'X'
,要么是'O'
,对于空 Square(方格) 则是null
。
接下来,我们需要修改当点击方格的时候会发生的事情。Board(棋盘) 组件现在存储了那些已经填充的方格,这意味着我们需要一些方法来使 Square(方格) 组件 更新 Board(棋盘) 组件的 state(状态) 。由于组件的 state(状态) 被认为是私有的,我们不能从 Square(方格) 组件直接更新 Board(棋盘) 的 state(状态) 。
通常的模式是将一个函数从 Board(棋盘) 组件 传递到 Square(方格) 组件上,该函数在方格被点击时调用。
再次修改 Board(棋盘) 组件中 renderSquare
,修改为:
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
注意:
我们将返回的元素拆分成多行以提高可读性,并在其两边加上括号,这样 JavaScript 就不会在
return
后插入分号并且破坏我们的代码了。
现在我们把 Board(棋盘) 组件中 2 个的 props(属性) 传递给 Square(方格) 组件:value
和 onClick
。onClick
prop(属性) 是一个函数,Square(方格) 组件 可以调用该函数。我们对 Square(方格) 组件进行以下更改:
- 在 Square(方格) 组件的
render
中用this.props.value
替换this.state.value
。 - 在 Square(方格) 组件的
render
中用this.props.onClick()
替换this.setState()
。 - 从 Square(方格) 组件删除
constructor
(构造函数) 定义,因为它不再需要 state(状态) 。
在这些修改之后,整个 Square(方格) 组件看起来像这样:
class Square extends React.Component {
render() {
return (
<button
className="square"
onClick={() => this.props.onClick()}
>
{this.props.value}
</button>
);
}
}
现在当方格被点击时,它调用由 Board(棋盘) 组件传递的 onClick
函数。让我们回顾一下这里发生了什么:
- 内置 DOM 的
<button>
组件上的onClick
prop(属性) 告诉 React 设置一个 click 事件侦听器。 - 当点击按钮时,React 将调用在 Square(方格) 组件
render()
方法中定义的onClick
事件处理程序。 - 这个事件处理程序调用
this.props.onClick()
。 Square(方格) 组件的onClick
props(属性) 由 Board(棋盘) 组件指定。 - Board(棋盘) 组件将
onClick={() => this.handleClick(i)}
传递给 Square(方格) 组件,所以当被调用时,它会在 Board(棋盘) 组件 上运行this.handleClick(i)
。 - 我们还没有在 Board(棋盘) 组件 上定义
handleClick()
方法,所以导致代码崩溃。
注意:
DOM
<button>
元素的onClick
属性对于 React 来说有特殊意义,因为它是一个内置组件。 对于像 Square(方格) 这样的自定义组件,命名取决于您自己。我们可以用不同的方式命名 Square(方格) 的onClick
prop(属性) 或 Board(棋盘) 的handleClick
方法。在React中,对于表示事件的 props(属性) 使用on[Event]
命名,对处理事件的方法使用handle[Event]
。
当我们尝试点击一个 Square(方格) 时,
我们应该收到一个错误,因为我们还没有定义 handleClick
。
我们现在将handleClick
添加到 Board(棋盘) 类:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = 'X';
this.setState({squares: squares});
}
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
const status = 'Next player: X';
return (
<div>
<div className="status">{status}</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>
);
}
}
在这些修改之后,我们就能够再次点击 Squares(方格) 来填充它们。 但是,现在 state(状态) 已经存储在 Board(棋盘) 组件中而不是单个 Square(方格) 组件中。 当 Board(棋盘) 的状态发生变化时, Square(方格) 组件会自动重新渲染。 保持 Board(棋盘) 组件中所有 Square(方格) 组件的 state(状态) 将允许它在将来确定胜利者。
由于 Square(方格) 组件不再保存 state(状态) , Square(方格) 组件从 Board(棋盘) 组件接收值,并在单击它们时通知 Board(棋盘) 组件。 在 React 术语中, Square(方格) 组件现在是 受控组件 。 Board(棋盘) 会完全控制他们。
注意在 handleClick
中,我们调用 .slice()
来创建 squares
数组的副本来修改,而不是修改现有的数组。
我们将在下一节中解释为什么我们要创建 squares
数组的副本。
不可变数据的重要性
在前面的代码示例中,我们建议在数组进行更改之前使用 .slice()
运算符来创建 squares
数组的副本以进行修改,而不是修改现有数组。 我们现在将讨论 不可变性 以及为什么不可变性 很重要。
通常有两种方式来更改数据。第一种方法是通过直接更改变量的值来 改变 数据。第二种方法是用包含所需更改对象的新副本来替换数据。
通过赋值改变数据(Data change with mutation)
var player = {score: 1, name: 'Jeff'};
player.score = 2;
// 现在 player 是 {score: 2, name: 'Jeff'}
不通过赋值改变数据(Data change without mutation)
var player = {score: 1, name: 'Jeff'};
var newPlayer = Object.assign({}, player, {score: 2});
// 现在 player 没改变, 但是 newPlayer 是 {score: 2, name: 'Jeff'}
// 或者如果你使用对象扩展语法,可以写成:
// var newPlayer = {...player, score: 2};
最终结果是相同的,但通过不直接改变数据(或更改底层数据)有一个额外的好处,可以帮助我们增强组件和整体应用性能。
复杂的功能变得简单
不可变性(Immutability) 可以使复杂功能更容易实现。在本教程的后面,我们将实现一个 “过程回放” 功能,允许我们查看井字游戏的历史记录并 “跳回” 以前的动作(悔棋)。 此功能并非只限于特定游戏 — 撤消和重做某些操作的功能是应用程序中的常见要求。避免直接修改数据可以让我们保留游戏历史的先前版本,并在以后重复使用。
检测变更(Detecting Changes)
检测可变对象的变化很困难,因为它们是直接修改的。该检测需要将可变对象与其自身的先前副本进行比较, 以及要遍历的整个对象树。
检测不可变对象中的更改要容易得多。 如果被引用的不可变对象与前一个不同,则该对象已更改。仅此而已。
确定何时重新渲染(Determining When to Re-render in React)
React 中不可变数据最大好处在于当您构建简单的 纯(pure)组件 时。由于不可变性(Immutability) 可以更容易地确定是否已经进行了更改,这也有助于确定组件何时需要重新渲染。
要了解有关 shouldComponentUpdate()
的更多信息,以及如何构建 纯(pure)组件 ,请查看优化性能。
函数式组件(Functional Components)
我们现在将 Square(方格) 改为 函数式组件(Functional Components) 。
在 React 中,**函数式组件(Functional Components) 是一种更简单的方法来编写只包含 render
方法且没有自己 state(状态) 的组件。
而不是定义一个 React.Component
的扩展类,
我们可以编写一个函数,它将 props
作为输入,并返回应该渲染的内容。
函数式组件写入比类更乏味,
并且可以用这种方式表达许多组件。
用这个函数替换 Square(方格) 类:
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
我们将两次出现的 this.props
都改为 props
。
注意:
当我们将 Square 修改为一个函数式组件时,我们还将
onClick={() => this.props.onClick()}
更改为更短的onClick = {props.onClick}
(注意两边都去掉了括号)。 在类中,我们使用箭头函数来访问正确的this
值,但在函数式组件中我们不需要担心this
。
轮流下棋(Taking Turns)
我们现在需要在我们的井字游戏中修复一个明显的缺陷:无法在棋盘上标记”O”。
我们默认将第一步下棋动作为 “X” 。 我们可以通过修改 Board(棋盘) 构造函数中的初始 state(状态) 来设置此默认值:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
每次下棋,我们应该切换 xIsNext
布尔值,并保存 state(状态) 。现在更新 Board(棋盘) 组件的 handleClick
函数来反转 xIsNext
的值。
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
修改后,“X” 和 “O” 可以轮流切换了。接下来,修改 Board(棋盘) 组件 render
方法中的 “status” 文本,以便它还显示下一个下棋的是谁。
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
// 其余的代码没有修改
这些更改后, Board(棋盘) 组件应该是这样的:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
<div>
<div className="status">{status}</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>
);
}
}
宣布获胜者(Declaring a Winner)
现在我们展示下一个玩家的转换, 我们还应该展示何时赢得比赛,并且结束比赛。 我们可以通过将此帮助函数添加到文件末尾来确定获胜者:
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;
}
我们将在 Board(棋盘) 组件的 render
函数中调用 calculateWinner(squares)
以检查玩家是否赢了。
如果玩家获胜,我们可以显示诸如 “Winner: X” 或 “Winner: O” 之类的文本。
我们将使用以下代码替换 Board(棋盘) 的render
函数中的 status
声明:、
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
// 其余的代码没有修改
现在,如果有人已经赢得了比赛,或者当一个方格已经被填充时,你需要提前返回,并且忽略该点击。 你可以修改 Board(棋盘) 组件的 handleClick
:
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
恭喜! 你现在已经有了一个能正常工作的 tic-tac-toe 游戏。 并且现在你知道了 React 的基础知识。 所以 你可能是真正的赢家 。
添加过程回放
作为最后的练习,让我们添加过程回放功能,以回到之前的游戏动作。
存储历史记录
如果我们改变了 square
数组,实现过程回放将非常困难。
但是,我们使用 slice()
在每次移动后创建 squares
数组的新副本,
并将其视为不可变的。
这将允许我们存储 square
数组的每个过去版本,
并在已经发生的玩家轮换之间切换。
我们将过去的square
数组存储在另一个名为history
的数组中。
history
数组代表所有 Board(棋盘) 的 state(状态),
从第一步到最后一步,
并有这样的数据:
history = [
// Before first move
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// After first move
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// After second move
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
现在我们需要决定哪个组件应该拥有 history
state(状态)。
再次 State(状态) 提升
我们希望 Game(游戏) 组件显示过去动作列表。
它需要访问 history
来做到这一点,
所以我们将 history
state(状态) 放在顶级 Game(游戏) 组件中。
将 history
state(状态) 放入 Game(游戏) 组件可以让我们从子 Board(棋盘) 组件中删除 squares
state(状态) 。
就像我们将 Square(方格)组件 中的 “state(状态)提升” 到 Board(棋盘)组件一样,我们现在将其从 Board(棋盘) 组件 提升到顶级 Game(游戏) 组件中。
这使 Game(游戏) 组件可以完全控制 Board(棋盘) 的数据,
并让它根据 history
显示 Board(棋盘) 的纪录。
首先,通过添加一个构造函数来设置 Game(游戏) 组件的初始 state(状态) :
class Game extends React.Component {
constructor() {
super();
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
接下来,我们将让 Board(棋盘) 组件从Game(游戏) 组件接收 squares
和 onClick
prop(属性) 。
因为我们现在在 Board(棋盘) 中有一个单击处理程序用于许多 Square(方格) ,
我们需要将每个 Square(方格) 的索引位置传递给 onClick
处理程序,以指示单击了哪个 Square(方格) 。
以下是转换 Board(棋盘) 组件所需的步骤:
- 删除 Board(棋盘) 组件中的
constructor
(构造函数)。 - 在 Board(棋盘) 组件的
renderSquare
中用this.props.squares[i]
替换this.state.squares[i]
。 - 在 Board(棋盘) 组件的
renderSquare
中用this.props.onClick(i)
替换this.handleClick(i)
。
现在整个 Board(棋盘) 组件看起来像这样:
class Board extends React.Component {
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div>
<div className="status">{status}</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>
);
}
}
Game(游戏) 组件的 render
应该可以获取最近的历史记录,并可以接管计算游戏状态:
render() {
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.xIsNext ? 'X' : 'O');
}
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>
);
}
由于 Game(游戏) 组件现在渲染游戏的状态,
我们可以从 Board(棋盘) 的render
方法中删除相应的代码。
重构后, Board(棋盘) 的render
函数如下所示:
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>
);
}
最后,我们需要将 handleClick
方法从 Board(棋盘) 组件移动到 Game(游戏) 组件。
我们还需要修改 handleClick
,因为 Game(游戏) 组件的 state(状态) 有结构上的不同。
在 Game(游戏) 的handleClick
方法中,
我们将新的历史条目连接到history
。
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.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
}]),
xIsNext: !this.state.xIsNext,
});
}
注意:
与你所熟悉的数组
push()
方法不同,concat()
方法不会改变原始数组,所以我们更喜欢它。
到目前位置, Board(棋盘) 组件只需要 renderSquare
和 render
方法。 这个游戏的 state(状态) 和 handleClick
方法应该都在 Game(游戏) 组件中。
显示过去的动作
由于我们正在记录井字游戏的历史,我们现在可以将其作为过去的动作列表显示给玩家。
我们之前了解到 React 元素是一等的JavaScript对象; 我们可以在我们的应用程序中传递它们。 要在React中渲染多个项目,我们可以使用 React 元素数组。
在JavaScript中,数组有一个map()
方法 ,它通常用于映射数据到其他数据,例如:
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]
使用map
方法,我们可以将我们的下棋动作历史映射到屏幕中按钮表示的 React 元素上,
并显示一个按钮列表以“跳转”到过去的动作。
让我们在 Game(游戏) 组件的 render
方法中对 history
进行 map
:
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
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>{moves}</ol>
</div>
</div>
);
}
对于游戏历史中的每一步动作,
我们创建一个列表项<li>
,其中包含一个按钮<button>
。
该按钮有一个onClick
处理程序,它调用一个名为 this.jumpTo()
的方法。
我们还没有实现jumpTo()
方法。
现在,我们应该看到游戏中发生的每一步动作列表,以及一条警告:
警告: Each child in an array or iterator should have a unique “key” prop. Check the render method of “Game”.(数组或迭代器中的每个子元素都应该有一个唯一的 “key” prop。请检查 “Game” 的渲染方法。)
我们来谈谈这个警告是什么意思。
选择一个 Key
当我们渲染列表时,React 总是存储有关每个渲染列表项的一些信息。 当您更新该列表时,React 需要确定发生了什么变化。您可以在列表中添加,删除,重新排列或更新项目。
想象列表从
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
更新为
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
用眼睛看,看来 Alexa 和 Ben 很有可能交换了地方,而 Claudia 被添加进来 - 但是 React 只是一个计算机程序,不知道你打算做什么。因此,React 要求您在列表中的每个元素上指定一个 key(健) 属性(property),一个字符串来区分每个组件与其兄弟组件。在这种情况下, alexa
, ben
, claudia
可能是作为 key(健) 的明智选择;如果项目对应于数据库中的对象,数据库 ID 通常是一个不错的选择:
<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(健) 被删除时,一个组件将会被销毁。 key(健) 用来告诉 React 关于每个组件的身份标识 ,因此他可以维持 state(状态) 到重新渲染。如果更改组件的 key(健) ,它将被完全毁灭,并重新建立一个新的 state(状态) 。
强烈建议你在构建动态列表时分配适当的 key(健) 。 如果你没有适当的值来作为 key(健) ,您可能需要考虑重组你的数据,以便你这样做。
如果您没有指定任何 key(健) ,React 会警告你,并回到使用数组索引作为 key(健) - 这不是正确的选择,如果您重新排序列表中的元素 或 在列表的底部的任何位置添加/删除项目。明确地传递 key={i}
可以使警告消失,但有问题同样存在,因此在大多数情况下不推荐这么使用。
组件 key(健) 不需要是全局唯一的,只要相对于直系兄弟元素是唯一的就行。
实现过程回放
在井字游戏的历史记录中,每个过去的动作都有一个与之相关的唯一ID:它是动作的连续编号。 动作永远不会重新排序,删除或从中间插入, 所以使用动作索引作为 key 是安全的。
在 Game(游戏) 组件的render
方法中,我们可以将 key 添加为<li key = {move}>
,并且 React 关于 key 的警告应该消失:
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
方法未定义。
在我们实现 jumpTo
之前,
我们将 stepNumber
添加到 Game(游戏) 组件的 state(状态) 中,以指示我们当前正在查看的步骤。
首先,在 Game(游戏) 组件 constructor
(构造函数)的初始 state(状态) 中添加 stepNumber: 0
:
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
xIsNext: true,
};
}
接下来,我们将在 Game(游戏) 组件中定义 jumpTo
方法来更新该 state(状态) 。我们也想更新 xIsNext
。如果步骤编号的索引为偶数,则将 xIsNext
设置为 true
。
在 Game(游戏) 类中添加一个名为 jumpTo
的方法:
handleClick(i) {
// this method has not changed
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0,
});
}
render() {
// this method has not changed
}
我们现在将对 Game(游戏)的 handleClick
方法进行一些更改,当您单击一个 Square(方格) 时会触发该方法。
我们添加的 stepNumber
state(状态) 反映了现在向用户显示的动作。 在我们采取新举措后,
我们需要通过添加 stepNumber:history.length
作为 this.setState
参数的一部分来更新 stepNumber
。
这样可以确保我们不会在制作新动作后显示相同的动作。
我们还将用 this.state.history.slice(0, this.state.stepNumber + 1)
替换 this.state.history
读取。
这确保了如果我们进行了过程回放,然后从那一步开始新的动作,我们抛弃了所有后续的记录。
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
方法,始终根据 stepNumber
渲染最后一次动作以呈现当前选定的动作:
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
// the rest has not changed
如果我们点击游戏历史中的任何一步,棋盘应该立即更新,以显示该步骤发生后棋面的情况。
总结(Wrapping Up)
现在,你已经完成了一个 tic-tac-toe 游戏:
- 让你玩 tic-tac-toe ,
- 指示一个玩家是否赢得比赛,
- 存储游戏中的历史动作,
- 让玩家及时跳回去看某一步的游戏棋盘。
我们希望你现在觉得你对 React 的工作流程有一个很好的把握。
查看最终结果:最终结果。
如果你有额外的时间或想要练习你的新技能,这里有一些可以做出改进的想法,按照难度越来越高的顺序列出:
- 在动作的历史记录列表中以(列,行)的格式显示每个下棋动作的位置。
- 在步骤列表中加粗显示当前选中的项目。
- 重写 Board(棋盘) 使用两个循环来制作方格,而不是对它们进行硬编码。
- 添加一个切换按钮,您可以按升序或降序对步骤列表进行排序。
- 当有人赢了,突出显示导致游戏胜利的三个方块。
- 当没有人获胜时,显示关于结果为平局的消息。
在本教程中,我们已经触及了许多 React 概念,包括元素,组件,props 和 state 。
有关这些主题的更深入的解释,请查看 完整的文档。
要了解有关定义组件的更多信息,请查看React.Component
API 参考。