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通过从onClickSquare 方法中的处理程序调用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 都会收到一个valueprop,它可以是'X'、'O'或null空方格。
接下来,我们需要更改单击 Square 时发生的情况。Board 组件现在维护填充了哪些方格。我们需要为 Square 创建一种更新 Board 状态的方法。由于 state 被认为是定义它的组件私有的,我们不能直接从 Square 更新 Board 的 state。
相反,我们将一个函数从 Board 传递给 Square,当单击一个正方形时,我们将让 Square 调用该函数。我们将renderSquareBoard 中的方法更改为:
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)} />
);
}
笔记
为了便于阅读,我们将返回的元素分成多行,并添加了括号,这样 JavaScript 就不会在后面插入分号
return并破坏我们的代码。
现在我们将两个道具从 Board 传递到 Square:value和onClick. onClickprop 是 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 的onClickprop 或 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”之类的文本。我们将使用以下代码替换statusBoardrender函数中的声明:
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 组件所需的步骤:
- 删除
constructorin 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 组件只需要renderSquareandrender方法。游戏的状态和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数组时,stepvariable 指的是当前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