什么是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>
   );
 }
}

前:

image-20220422214948223

之后:您应该在渲染输出的每个方块中看到一个数字。

image-20220422215005760

此时查看完整代码

恭喜!您刚刚从父 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,您应该会在浏览器的开发工具控制台中看到“单击”。

笔记

为了节省输入并避免 的混淆行为this,我们将在此处和以下进一步使用事件处理程序的箭头函数语法:

 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'})}.
  • classNameonClick道具放在不同的行上以提高可读性。

在这些更改之后,<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 也会自动更新其中的子组件。

此时查看完整代码

开发者工具

适用于ChromeFirefox的 React Devtools 扩展允许您使用浏览器的开发人员工具检查 React 组件树。

image-20220422215029319

React DevTools 可让您检查 React 组件的 props 和状态。

安装 React DevTools 后,您可以右键单击页面上的任何元素,单击“Inspect”打开开发者工具,React 选项卡(“⚛️ Components”和“⚛️ Profiler”)将作为最后一个选项卡出现在对。使用“⚛️ Components”检查组件树。

但是,请注意,还有一些额外的步骤可以让它与 CodePen 一起使用:

  1. 登录或注册并确认您的电子邮件(需要防止垃圾邮件)。
  2. 单击“分叉”按钮。
  3. 单击“更改视图”,然后选择“调试模式”。
  4. 在打开的新选项卡中,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:valueonClickonClickprop 是 Square 在单击时可以调用的函数。我们将对 Square 进行以下更改:

  • 在 Square 的方法中替换this.state.valuethis.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 提供的函数。以下是如何实现这一点的回顾:

  1. onClick内置 DOM 组件上的prop<button>告诉 React 设置一个点击事件监听器。
  2. 单击按钮时,React 将调用Square方法onClick中定义的事件处理程序。render()
  3. 此事件处理程序调用this.props.onClick(). Square 的onClick道具由董事会指定。
  4. 由于 Board 传递onClick={() => this.handleClick(i)}给 Square,Square 会handleClick(i)在单击时调用 Board。
  5. 我们还没有定义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.propsprops它出现的两次。

此时查看完整代码

笔记

当我们将 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 squaresonClick由于我们现在在 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 无法知道我们的意图,所以我们需要为每个列表项指定一个_键_属性,以区分每个列表项与其兄弟项。一种选择是使用字符串alexabenclaudia。如果我们显示来自数据库的数据,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可能看起来它属于propskey但不能使用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.historythis.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 技能,这里有一些您可以对井字游戏进行改进的想法,这些想法按难度递增的顺序列出:

  1. 以移动历史列表中的格式(列、行)显示每个移动的位置。
  2. 将移动列表中当前选定的项目加粗。
  3. 重写 Board 以使用两个循环来制作正方形,而不是对它们进行硬编码。
  4. 添加一个切换按钮,可让您按升序或降序对移动进行排序。
  5. 当有人获胜时,突出显示导致获胜的三个方格。
  6. 当没有人获胜时,显示一条关于结果为平局的消息。