React 初学者: 教程:React 简介 五
什么是React?
React 是一个用于构建用户界面的声明式、高效且灵活的 JavaScript 库。它使您可以从称为“组件”的小而孤立的代码组成复杂的 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 ... */)
);
如果您好奇,API 参考createElement()
中有更详细的描述,但我们不会在本教程中使用它。相反,我们将继续使用 JSX。
JSX 具有 JavaScript 的全部功能。您可以将_任何_JavaScript 表达式放在 JSX 中的大括号内。每个 React 元素都是一个 JavaScript 对象,您可以将其存储在变量中或在程序中传递。
上面的ShoppingList
组件只渲染内置的 DOM 组件,例如<div />
和<li />
。但是你也可以编写和渲染自定义的 React 组件。例如,我们现在可以通过编写来引用整个购物清单<ShoppingList />
。每个 React 组件都经过封装,可以独立运行;这允许您从简单的组件构建复杂的 UI。
检查入门代码
如果您要在浏览器中处理本教程,请在新选项卡中打开此代码:Starter Code。如果您要在**本地处理本教程,**请在您的项目文件夹中打开(在设置src/index.js
过程中您已经接触过此文件)。
此入门代码是我们正在构建的基础。我们提供了 CSS 样式,因此您只需专注于学习 React 和编程井字游戏。
通过检查代码,你会注意到我们有三个 React 组件:
- 正方形
- 木板
- 游戏
Square 组件渲染单个<button>
,Board 渲染 9 个正方形。Game 组件使用占位符值渲染一个棋盘,稍后我们将对其进行修改。当前没有交互式组件。
通过道具传递数据
为了让我们的脚湿透,让我们尝试将一些数据从我们的 Board 组件传递到我们的 Square 组件。
我们强烈建议您在学习本教程时手动输入代码,而不是使用复制/粘贴。这将帮助您发展肌肉记忆和更强的理解力。
在 Board 的renderSquare
方法中,更改代码以将调用的道具传递value
给 Square:
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />; }
}
更改 Square 的方法以通过替换render
来显示该值:{/* TODO */}``````{this.props.value}
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value} </button>
);
}
}
前:
之后:您应该在渲染输出的每个方块中看到一个数字。
恭喜!您刚刚从父 Board 组件“传递了一个 prop”到子 Square 组件。传递 props 是 React 应用程序中信息从父母到孩子的流动方式。
制作交互式组件
当我们单击它时,让我们用“X”填充 Square 组件。首先,将从 Square 组件的render()
函数返回的按钮标签更改为:
class Square extends React.Component {
render() {
return (
<button className="square" onClick={function() { console.log('click'); }}> {this.props.value}
</button>
);
}
}
如果您现在单击 Square,您应该会在浏览器的开发工具控制台中看到“单击”。
笔记
class Square extends React.Component {
render() {
return (
<button className="square" onClick={() => console.log('click')}> {this.props.value}
</button>
);
}
}
请注意
onClick={() => console.log('click')}
,我们如何将_函数_作为onClick
道具传递。React 只会在点击后调用这个函数。忘记() =>
和写入onClick={console.log('click')}
是一个常见的错误,并且会在每次组件重新渲染时触发。
下一步,我们希望 Square 组件“记住”它被点击过,并用“X”标记填充它。为了“记住”事物,组件使用state。
React 组件可以通过this.state
在其构造函数中设置来获得状态。this.state
应该被认为是其定义的 React 组件的私有。让我们将 Square 的当前值存储在 中this.state
,并在单击 Square 时更改它。
首先,我们将在类中添加一个构造函数来初始化状态:
class Square extends React.Component {
constructor(props) { super(props); this.state = { value: null, }; }
render() {
return (
<button className="square" onClick={() => console.log('click')}>
{this.props.value}
</button>
);
}
}
笔记
在JavaScript 类中,您需要
super
在定义子类的构造函数时始终调用。所有具有 a 的 React 组件类都constructor
应该以super(props)
调用开头。
现在我们将更改 Square 的render
方法以在单击时显示当前状态的值:
- 替换
this.props.value
为标签this.state.value
内。<button>
- 将
onClick={...}
事件处理程序替换为onClick={() => this.setState({value: 'X'})}
. - 将
className
和onClick
道具放在不同的行上以提高可读性。
在这些更改之后,<button>
Square 的render
方法返回的标签如下所示:
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>
);
}
}
this.setState
通过从onClick
Square 方法中的处理程序调用render
,我们告诉 React 在单击 Square 时重新渲染它<button>
。更新后,Squarethis.state.value
将是'X'
,所以我们会X
在游戏板上看到 。如果您单击任何 Square,X
应该会出现一个。
当你调用setState
一个组件时,React 也会自动更新其中的子组件。
开发者工具
适用于Chrome和Firefox的 React Devtools 扩展允许您使用浏览器的开发人员工具检查 React 组件树。
React DevTools 可让您检查 React 组件的 props 和状态。
安装 React DevTools 后,您可以右键单击页面上的任何元素,单击“Inspect”打开开发者工具,React 选项卡(“⚛️ Components”和“⚛️ Profiler”)将作为最后一个选项卡出现在对。使用“⚛️ Components”检查组件树。
但是,请注意,还有一些额外的步骤可以让它与 CodePen 一起使用:
- 登录或注册并确认您的电子邮件(需要防止垃圾邮件)。
- 单击“分叉”按钮。
- 单击“更改视图”,然后选择“调试模式”。
- 在打开的新选项卡中,devtools 现在应该有一个 React 选项卡。
完成游戏
我们现在有了井字游戏的基本构建块。为了有一个完整的游戏,我们现在需要在棋盘上交替放置“X”和“O”,我们需要一种方法来确定获胜者。
提升状态
目前,每个 Square 组件都维护着游戏的状态。为了检查获胜者,我们将在一个位置保留 9 个方格中每个方格的值。
我们可能认为董事会应该只向每个 Square 询问 Square 的状态。尽管这种方法在 React 中是可行的,但我们不鼓励它,因为代码变得难以理解、容易出现错误并且难以重构。相反,最好的方法是将游戏的状态存储在父 Board 组件中,而不是存储在每个 Square 中。Board 组件可以通过传递一个 prop 来告诉每个 Square 要显示什么,就像我们向每个 Square 传递一个数字时所做的那样。
要从多个子组件收集数据,或让两个子组件相互通信,您需要在其父组件中声明共享状态。父组件可以使用 props 将状态向下传递给子组件;这使子组件相互之间以及与父组件保持同步。
当 React 组件被重构时,将状态提升到父组件中是很常见的——让我们借此机会尝试一下。
向 Board 添加一个构造函数,并将 Board 的初始状态设置为包含对应于 9 个正方形的 9 个空数组:
class Board extends React.Component {
constructor(props) { super(props); this.state = { squares: Array(9).fill(null), }; }
renderSquare(i) {
return <Square value={i} />;
}
当我们稍后填写板时,this.state.squares
数组将如下所示:
[
'O', null, 'X',
'X', 'X', 'O',
'O', null, null,
]
Board 的renderSquare
方法目前如下所示:
renderSquare(i) {
return <Square value={i} />;
}
一开始,我们将道具从 Board 向下传递,value
以在每个 Square 中显示从 0 到 8 的数字。在之前的不同步骤中,我们将数字替换为由 Square 自身状态确定的“X”标记。这就是为什么 Square 目前忽略value
了董事会传递给它的道具。
我们现在将再次使用道具传递机制。我们将修改 Board 以指示每个单独的 Square 关于其当前值('X'
、'O'
或null
)。我们已经squares
在 Board 的构造函数中定义了数组,我们将修改 Board 的renderSquare
方法以从中读取:
renderSquare(i) {
return <Square value={this.state.squares[i]} />; }
现在,每个 Square 都会收到一个value
prop,它可以是'X'
、'O'
或null
空方格。
接下来,我们需要更改单击 Square 时发生的情况。Board 组件现在维护填充了哪些方格。我们需要为 Square 创建一种更新 Board 状态的方法。由于 state 被认为是定义它的组件私有的,我们不能直接从 Square 更新 Board 的 state。
相反,我们将一个函数从 Board 传递给 Square,当单击一个正方形时,我们将让 Square 调用该函数。我们将renderSquare
Board 中的方法更改为:
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)} />
);
}
笔记
为了便于阅读,我们将返回的元素分成多行,并添加了括号,这样 JavaScript 就不会在后面插入分号
return
并破坏我们的代码。
现在我们将两个道具从 Board 传递到 Square:value
和onClick
. onClick
prop 是 Square 在单击时可以调用的函数。我们将对 Square 进行以下更改:
- 在 Square 的方法中替换
this.state.value
为this.props.value``````render
- 在 Square 的方法中替换
this.setState()
为this.props.onClick()``````render
- 从 Square中删除,
constructor
因为 Square 不再跟踪游戏的状态
在这些更改之后,Square 组件如下所示:
class Square extends React.Component { render() { return (
<button
className="square"
onClick={() => this.props.onClick()} >
{this.props.value} </button>
);
}
}
单击 Square 时,onClick
会调用 Board 提供的函数。以下是如何实现这一点的回顾:
onClick
内置 DOM 组件上的prop<button>
告诉 React 设置一个点击事件监听器。- 单击按钮时,React 将调用Square方法
onClick
中定义的事件处理程序。render()
- 此事件处理程序调用
this.props.onClick()
. Square 的onClick
道具由董事会指定。 - 由于 Board 传递
onClick={() => this.handleClick(i)}
给 Square,Square 会handleClick(i)
在单击时调用 Board。 - 我们还没有定义
handleClick()
方法,所以我们的代码崩溃了。如果你现在点击一个方块,你应该会看到一个红色的错误屏幕,上面写着“this.handleClick is not a function”。
笔记
DOM
<button>
元素的onClick
属性对 React 具有特殊的意义,因为它是一个内置组件。对于像 Square 这样的自定义组件,命名由您决定。我们可以给 Square 的onClick
prop 或 Board 的handleClick
方法起任何名字,代码也一样。在 React 中,通常为on[Event]
表示事件的 props 和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>
);
}
}
在这些更改之后,我们再次能够单击方块来填充它们,就像我们以前一样。但是,现在状态存储在 Board 组件中,而不是单独的 Square 组件中。当 Board 的状态发生变化时,Square 组件会自动重新渲染。保持 Board 组件中所有方格的状态将使其能够确定未来的获胜者。
由于 Square 组件不再保持状态,因此 Square 组件从 Board 组件接收值并在单击时通知 Board 组件。在 React 术语中,Square 组件现在是受控组件。董事会对其拥有完全控制权。
请注意如何在 中handleClick
,我们调用.slice()
创建squares
要修改的数组的副本,而不是修改现有数组。squares
我们将在下一节解释为什么要创建数组的副本。
为什么不变性很重要
在前面的代码示例中,我们建议您squares
使用该slice()
方法创建数组的副本,而不是修改现有数组。我们现在将讨论不变性以及为什么学习不变性很重要。
更改数据通常有两种方法。第一种方法是通过直接_更改_数据的值来改变数据。第二种方法是用具有所需更改的新副本替换数据。
突变的数据变化
var player = {score: 1, name: 'Jeff'};
player.score = 2;
// Now player is {score: 2, name: 'Jeff'}
无突变的数据更改
var player = {score: 1, name: 'Jeff'};
var newPlayer = Object.assign({}, player, {score: 2});
// Now player is unchanged, but newPlayer is {score: 2, name: 'Jeff'}
// Or if you are using object spread syntax proposal, you can write:
// var newPlayer = {...player, score: 2};
最终结果是相同的,但通过不直接改变(或更改基础数据),我们获得了如下所述的几个好处。
复杂的功能变得简单
不变性使复杂的功能更容易实现。在本教程的后面,我们将实现一个“时间旅行”功能,它允许我们回顾井字游戏的历史并“跳回”到之前的动作。此功能并非特定于游戏 - 撤消和重做某些操作的能力是应用程序中的常见要求。避免直接的数据突变让我们可以保持游戏历史的先前版本完整,并在以后重用它们。
检测变化
检测可变对象的变化很困难,因为它们是直接修改的。这种检测需要将可变对象与其自身的先前副本进行比较,并需要遍历整个对象树。
检测不可变对象的变化要容易得多。如果被引用的不可变对象与前一个不同,则该对象已更改。
确定何时在 React 中重新渲染
不变性的主要好处是它可以帮助您在 React 中构建_纯组件。_不可变数据可以轻松确定是否进行了更改,这有助于确定组件何时需要重新渲染。
您可以通过阅读Optimizing Performance了解更多关于shouldComponentUpdate()
以及如何构建_纯组件_的信息。
功能组件
我们现在将 Square 更改为函数组件。
在 React 中,函数组件是一种更简单的编写组件的方法,它只包含一个render
方法并且没有自己的状态。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}
没有括号)。
轮流
我们现在需要修复井字游戏中的一个明显缺陷:无法在棋盘上标记“O”。
默认情况下,我们将第一步设置为“X”。我们可以通过修改 Board 构造函数中的初始状态来设置此默认值:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true, };
}
每次玩家移动时,xIsNext
(布尔值)将被翻转以确定下一个玩家并保存游戏状态。我们将更新 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
以便显示下一个回合的玩家:
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
// the rest has not changed
应用这些更改后,您应该拥有此 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>
);
}
}
宣布获胜者
既然我们显示了下一个回合是哪个玩家,我们还应该显示游戏何时获胜并且没有更多回合可做。复制此辅助函数并将其粘贴到文件末尾:
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;
}
给定一个由 9 个方格组成的数组,此函数将检查获胜者并根据需要返回'X'
、'O'
或null
。
我们将调用calculateWinner(squares)
Board 的render
函数来检查玩家是否获胜。如果玩家赢了,我们可以显示诸如“Winner: X”或“Winner: O”之类的文本。我们将使用以下代码替换status
Boardrender
函数中的声明:
render() {
const winner = calculateWinner(this.state.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); }
return (
// the rest has not changed
handleClick
如果有人赢了游戏或者广场已经被填满,我们现在可以通过忽略点击来更改 Board 的功能以提前返回:
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,
});
}
恭喜!您现在有一个有效的井字游戏。而且你也刚刚学习了 React 的基础知识。所以_你_可能是这里真正的赢家。
添加时间旅行
作为最后的练习,让我们可以“回到过去”到游戏中的先前动作。
存储移动历史
如果我们改变squares
数组,实现时间旅行将非常困难。
但是,我们习惯在每次移动后创建一个新的数组slice()
副本,并将其视为不可变的。这将允许我们存储数组的每个过去版本,并在已经发生的转弯之间导航。squares``````squares
我们会将过去的squares
数组存储在另一个名为history
. 该history
数组代表所有棋盘状态,从第一次移动到最后一次移动,并具有如下形状:
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
状态。
再次提升状态
我们希望顶级 Game 组件显示过去移动的列表。它需要访问 来history
做到这一点,所以我们将把history
状态放在顶级 Game 组件中。
将状态放入 Game 组件中可以让我们从其子 Board 组件history
中移除状态。squares
就像我们将状态从 Square 组件“提升”到 Board 组件一样,我们现在将它从 Board 提升到顶级 Game 组件。这使 Game 组件可以完全控制 Board 的数据,并让它指示 Board 从history
.
首先,我们将在其构造函数中设置 Game 组件的初始状态:
class Game extends React.Component {
constructor(props) { super(props); 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 组件的 props squares
。onClick
由于我们现在在 Board 中有一个用于许多 Squares 的单击处理程序,我们需要将每个 Square 的位置传递给onClick
处理程序以指示单击了哪个 Square。以下是转换 Board 组件所需的步骤:
- 删除
constructor
in Board。 - 替换
this.state.squares[i]
为this.props.squares[i]
板中的renderSquare
. - 替换
this.handleClick(i)
为this.props.onClick(i)
板中的renderSquare
.
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 组件的状态结构不同。在 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
andrender
方法。游戏的状态和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 元素,并显示一个按钮列表以“跳转”到过去的移动。
让我们map
结束history
游戏中的render
方法:
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>
);
}
当我们遍历history
数组时,step
variable 指的是当前history
元素的值,并且move
指的是当前history
元素的索引。我们只对move
这里感兴趣,因此step
没有被分配到任何东西。
对于井字游戏历史中的每一步,我们创建一个<li>
包含按钮的列表项<button>
。该按钮有一个onClick
处理程序,它调用一个名为 的方法this.jumpTo()
。我们还没有实现这个jumpTo()
方法。现在,我们应该看到游戏中发生的动作列表和开发者工具控制台中的警告:
警告:数组或迭代器中的每个孩子都应该有一个唯一的“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 的顺序,并在 Alexa 和 Ben 之间插入了 Claudia。然而,React 是一个计算机程序,并不知道我们的意图。因为 React 无法知道我们的意图,所以我们需要为每个列表项指定一个_键_属性,以区分每个列表项与其兄弟项。一种选择是使用字符串alexa
, ben
, claudia
。如果我们显示来自数据库的数据,Alexa、Ben 和 Claudia 的数据库 ID 可以用作键。
<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>
当一个列表被重新渲染时,React 获取每个列表项的键并在前一个列表项中搜索匹配的键。如果当前列表有一个以前不存在的键,React 会创建一个组件。如果当前列表缺少前一个列表中存在的键,React 会销毁前一个组件。如果两个键匹配,则移动相应的组件。键告诉 React 每个组件的身份,这允许 React 在重新渲染之间保持状态。如果组件的键发生变化,该组件将被销毁并以新状态重新创建。
key
是 React 中一个特殊的保留属性(与 一起ref
,一个更高级的特性)。创建元素时,React 会提取key
属性并将键直接存储在返回的元素上。尽管key
可能看起来它属于props
,key
但不能使用this.props.key
. React 自动使用key
来决定要更新哪些组件。组件无法查询其key
.
**强烈建议您在构建动态列表时分配正确的键。**如果您没有适当的密钥,您可能需要考虑重组数据以便您这样做。
如果没有指定键,React 将显示警告并默认使用数组索引作为键。在尝试重新排序列表项或插入/删除列表项时,使用数组索引作为键是有问题的。显式传递key={i}
会使警告静音,但与数组索引有相同的问题,在大多数情况下不建议使用。
密钥不需要是全局唯一的;它们只需要在组件及其同级之间是唯一的。
实施时间旅行
在井字游戏的历史中,过去的每一步都有一个与之关联的唯一 ID:它是移动的序号。移动永远不会在中间重新排序、删除或插入,因此使用移动索引作为键是安全的。
在 Game 组件的render
方法中,我们可以添加键 as<li key={move}>
并且 React 关于键的警告应该消失:
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
单击列表项的任何按钮都会引发错误,因为该jumpTo
方法未定义。在我们实现之前jumpTo
,我们将添加stepNumber
到 Game 组件的状态以指示我们当前正在查看的步骤。
首先,stepNumber: 0
在 Game’s 中添加初始状态constructor
:
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0, xIsNext: true,
};
}
接下来,我们将jumpTo
在 Game 中定义方法来更新stepNumber
. 如果我们要更改的数字是偶数,我们也设置xIsNext
为 true :stepNumber
handleClick(i) {
// this method has not changed
}
jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0, }); }
render() {
// this method has not changed
}
注意在jumpTo
方法中,我们没有更新history
状态的属性。这是因为状态更新是合并的,或者用更简单的话来说,React 将只更新方法中提到的属性,setState
保持剩余状态不变。有关更多信息**,请参阅文档**。
handleClick
我们现在将对当您单击一个正方形时触发的游戏方法进行一些更改。
stepNumber
我们添加的状态反映了现在向用户显示的移动。在我们做出新的举动之后,我们需要stepNumber
通过添加stepNumber: history.length
作为this.setState
参数的一部分来更新。这确保了我们不会在做出新动作后卡住显示相同的动作。
我们还将阅读替换this.state.history
为this.state.history.slice(0, this.state.stepNumber + 1)
。这确保了如果我们“回到过去”然后从那个点开始采取新的行动,我们就会抛弃所有现在不正确的“未来”历史。
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
如果我们点击游戏历史中的任何一步,井字棋棋盘应立即更新以显示棋盘在该步发生后的样子。
包起来
恭喜!您创建了一个井字游戏:
- 让你玩井字游戏,
- 表示玩家何时赢得比赛,
- 随着游戏的进行,存储游戏的历史,
- 允许玩家查看游戏历史并查看游戏棋盘的先前版本。
干得好!我们希望你现在觉得你对 React 的工作原理有了相当的了解。
在此处查看最终结果:最终结果。
如果您有额外的时间或想要练习新的 React 技能,这里有一些您可以对井字游戏进行改进的想法,这些想法按难度递增的顺序列出:
- 以移动历史列表中的格式(列、行)显示每个移动的位置。
- 将移动列表中当前选定的项目加粗。
- 重写 Board 以使用两个循环来制作正方形,而不是对它们进行硬编码。
- 添加一个切换按钮,可让您按升序或降序对移动进行排序。
- 当有人获胜时,突出显示导致获胜的三个方格。
- 当没有人获胜时,显示一条关于结果为平局的消息。
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/geek/post/react/save01/React-%E5%88%9D%E5%AD%A6%E8%80%85-%E6%95%99%E7%A8%8BReact-%E7%AE%80%E4%BB%8B-%E4%BA%94/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com