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


this.setState通过从onClickSquare 方法中的处理程序调用render,我们告诉 React 在单击 Square 时重新渲染它 ); }


我们已经改变```this.props```了```props```它出现的两次。

**[此时查看完整代码](https://zshipu.com/t?url=https://codepen.io/gaearon/pen/QvvJOv?editors=0010)**

> 笔记
> 
> 当我们将 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 ( this.handleClick(i)} /> ); }

render() { const status = ‘Next player: ‘ + (this.state.xIsNext ? ‘X’ : ‘O’); return (

{status}
{this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)}
{this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)}
{this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)}
); } }


**[此时查看完整代码](https://zshipu.com/t?url=https://codepen.io/gaearon/pen/KmmrBy?editors=0010)**

### 宣布获胜者

既然我们显示了下一个回合是哪个玩家,我们还应该显示游戏何时获胜并且没有更多回合可做。复制此辅助函数并将其粘贴到文件末尾:

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```Board```render```函数中的声明:

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()副本,并将其视为不可变的。这将允许我们存储数组的每个过去版本,并在已经发生的转弯之间导航。squaressquares

我们会将过去的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,
    });
  }

笔记


此时,Board 组件只需要```renderSquare```and```render```方法。游戏的状态和```handleClick```方法应该在 Game 组件中。

**[此时查看完整代码](https://zshipu.com/t?url=https://codepen.io/gaearon/pen/EmmOqJ?editors=0010)**

### 显示过去的动作

由于我们正在记录井字游戏的历史,我们现在可以将其作为过去移动的列表显示给玩家。

我们之前了解到 React 元素是一流的 JavaScript 对象。我们可以在我们的应用程序中传递它们。要在 React 中渲染多个项目,我们可以使用 React 元素数组。

在 JavaScript 中,数组有一个通常用于将数据映射到其他数据的[```map()```方法,例如:](https://zshipu.com/t?url=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/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 (

  • ); }); 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>
    

      {moves}