入门教程: 认识 React

课前准备

我们将会在这个教程中开发一个小游戏。你可能并不打算做游戏开发,然后就直接跳过了这个教程——但是不妨尝试一下!你将在该教程中学到关于构建 React 应用的基础知识,掌握这些知识后,你将会对 React 有更加深刻的理解。

提示

这篇教程适用于更喜欢边学边做的开发者,如果你更喜欢从头开始学习一些概念,请参考逐步学习指南。你会发现这篇教程和逐步学习指南是互补的。

这篇教程分为以下几个部分:

  • 环境准备是学习该教程的起点
  • 概览介绍了 React 的基础知识:组件、props 和 state。
  • 游戏完善介绍了在 React 开发过程中最常用的技术。
  • 时间旅行可以让你更加深刻地了解 React 的独特优势。

你不必着急一口气学完这篇教程的所有内容,尽力就行,哪怕是先学习一两节。

我们会做出什么东西?

接下来,我们一起用 React 开发一个井字棋(tic-tac-toe)。

你可以提前预览我们要写的游戏的最终效果。如果你看不懂其中的代码,或不熟悉这些语法,别担心!接下来的教程会一步一步帮助你理解 React 及其语法。

在继续后面的教程之前,推荐你先玩一下这个井字棋。在游戏里,你会看到游戏面板的右边有一个标有序号的列表。这个列表记录了游戏中的每个步骤,并且会随着游戏的进行不断更新。

等你熟悉游戏功能,便可关掉它!我们会在一个简单的模板上开始写起。下一步就是帮做好准备工作,这样你就可以开始游戏开发了。

前置知识

我们假定你已经对 HTML 和 JavaScript 都比较熟悉了。即便你之前使用其他编程语言,你也可以跟上这篇教程的。除此之外,我们假定你也已经熟悉了一些编程的概念,例如,函数、对象、数组,以及 class 的一些内容。

如果你想回顾一下 JavaScript,你可以阅读这篇教程。注意,我们也用到了一些 ES6(较新的 JavaScript 版本)的特性。在这篇教程里,我们主要使用了箭头函数(arrow functions)、class、let 语句和 const 语句。你可以使用 Babel REPL 在线预览 ES6 的编译结果。

环境准备

完成这篇教程有两种方式:可以直接在浏览器中编写代码,也可以在你电脑上搭建本地开发环境。

方式一:在浏览器中编写代码

这是上手最快的一种方式了!

首先在新的浏览器选项卡中打开这个初始模板。 你可以看到一个空的井字棋盘和 React 代码。我们接下来会在本教程中修改该 React 代码。

如果你选择这种方式,就可以跳过方式二,直接从概览开始阅读教程啦。

方式二:搭建本地开发环境

这是完全可选的,本教程不强制要求!

 

可选项:使用你喜欢的文本编辑器进行本地开发的步骤:

寻求帮助

如果你遇到了任何困难,可以去查看社区支持资源。你也可以在 Reactiflux Chat 快速求助。如果通过上述方式还是解决不了你的问题,请给我们提 issue,我们会帮助你的。

概览

你已经准备好了,让我们先大致了解一下 React 吧!

React 是什么?

React 是一个声明式,高效且灵活的用于构建用户界面的 JavaScript 库。使用 React 可以将一些简短、独立的代码片段组合成复杂的 UI 界面,这些代码片段被称作“组件”。

React 中拥有多种不同类型的组件,我们先从 React.Component 的子类开始介绍:

class ShoppingList extends React.Component {
  render() {
    return (
      

Shopping List for {this.props.name}

  • Instagram
  • WhatsApp
  • Oculus
); } } // 用法示例:

我们马上会讨论这些又奇怪、又像 XML 的标签。我们通过使用组件来告诉 React 我们希望在屏幕上看到什么。当数据发生改变时,React 会高效地更新并重新渲染我们的组件。

其中,ShoppingList 是一个 React 组件类,或者说是一个 React 组件类型。一个组件接收一些参数,我们把这些参数叫做 props(“props” 是 “properties” 简写),然后通过 render 方法返回需要展示在屏幕上的视图的层次结构。

render 方法的返回值描述了你希望在屏幕上看到的内容。React 根据描述,然后把结果展示出来。更具体地来说,render 返回了一个 React 元素,这是一种对渲染内容的轻量级描述。大多数的 React 开发者使用了一种名为 “JSX” 的特殊语法,JSX 可以让你更轻松地书写这些结构。语法 

 会被编译成 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 表达式,只需要用一个大括号把表达式括起来。每一个 React 元素事实上都是一个 JavaScript 对象,你可以在你的程序中把它当保存在变量中或者作为参数传递。

前文中的 ShoppingList 组件只会渲染一些内置的 DOM 组件,如

  • 等。但是你也可以组合和渲染自定义的 React 组件。例如,你可以通过  来表示整个购物清单组件。每个组件都是封装好的,并且可以单独运行,这样你就可以通过组合简单的组件来构建复杂的 UI 界面。

    阅读初始代码

    如果你要在浏览器中学习该教程,在新标签页中打开初始代码。如果你在本地环境中学习开发该教程的内容,就在你的工程文件夹下打开 src/index.js(你已经在前面的环境准备中创建过这个文件了)。

    这些初始代码是我们要开发的小游戏的基础代码。我们已经提供了 CSS 样式,这样你只需要关注使用 React 来开发这个井字棋了。

    通过阅读代码,你可以看到我们有三个 React 组件:

    • Square
    • Board
    • Game

    Square 组件渲染了一个单独的  ); } }

    修改前:

    入门教程: 认识 React_第1张图片

    修改后:在渲染结果中,你可以看到每个方格中都有一个数字。

    入门教程: 认识 React_第2张图片

    查看此步完整代码示例

    恭喜你!你刚刚成功地把一个 prop 从父组件 Board “传递”给了子组件 Square。在 React 应用中,数据通过 props 的传递,从父组件流向子组件。

    给组件添加交互功能

    接下来我们试着让棋盘的每一个格子在点击之后能落下一颗 “X” 作为棋子。 首先,我们把 Square 组件中 render() 方法的返回值中的 button 标签修改为如下内容:

    class Square extends React.Component {
      render() {
        return (
          
        );
      }
    }

    如果此刻点击某个格子,浏览器会弹出提示框。

    注意

    为了少输入代码,同时为了避免 this 造成的困扰,我们在这里使用箭头函数 来进行事件处理,如下所示:

    class Square extends React.Component {
     render() {
       return (
         
       );
     }
    }

    注意:此处使用了 onClick={() => alert('click')} 的方式向 onClick 这个 prop 传入一个函数。 React 将在单击时调用此函数。但很多人经常忘记编写 () =>,而写成了 onClick={alert('click')},这种常见的错误会导致每次这个组件渲染的时候都会触发弹出框。

    接下来,我们希望 Square 组件可以“记住”它被点击过,然后用 “X” 来填充对应的方格。我们用 state 来实现所谓“记忆”的功能。

    可以通过在 React 组件的构造函数中设置 this.state 来初始化 state。this.state 应该被视为一个组件的私有属性。我们在 this.state 中存储当前每个方格(Square)的值,并且在每次方格被点击的时候改变这个值。

    首先,我们向这个 class 中添加一个构造函数,用来初始化 state:

    class Square extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          value: null,
        };
      }
    
      render() {
        return (
          
        );
      }
    }

    注意

    在 JavaScript class 中,每次你定义其子类的构造函数时,都需要调用 super 方法。因此,在所有含有构造函数的的 React 组件中,构造函数必须以 super(props) 开头。

    现在,我们来修改一下 Square 组件的 render 方法,这样,每当方格被点击的时候,就可以显示当前 state 的值了:

    • 在 
    • 将 onClick={...} 事件监听函数替换为 onClick={() => this.setState({value: 'X'})}
    • 为了更好的可读性,将 className 和 onClick 的 prop 分两行书写。

    修改之后,Square 组件中 render 方法的返回值中的  ); } }

    在 Square 组件 render 方法中的 onClick 事件监听函数中调用 this.setState,我们就可以在每次  ); } }

    每一个 Square 被点击时,Board 提供的 onClick 函数就会触发。我们回顾一下这是怎么实现的:

    1. 向 DOM 内置元素 
    2. 当 button 被点击时,React 会调用 Square 组件的 render() 方法中的 onClick 事件处理函数。
    3. 事件处理函数触发了传入其中的 this.props.onClick() 方法。这个方法是由 Board 传递给 Square 的。
    4. 由于 Board 把 onClick={() => this.handleClick(i)} 传递给了 Square,所以当 Square 中的事件处理函数触发时,其实就是触发的 Board 当中的 this.handleClick(i) 方法。
    5. 现在我们还尚未定义 handleClick() 方法,所以代码还不能正常工作。如果此时点击 Square,你会在屏幕上看到红色的错误提示,提示内容为:“this.handleClick is not a function”。

    注意

    因为 DOM 元素 

    这时候我们点击 Square 的时候,浏览器会报错,因为我们还没有定义 handleClick 方法。我们现在来向 Board 里添加 handleClick 方法:

    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 (
           this.handleClick(i)}
          />
        );
      }
    
      render() {
        const status = 'Next player: X';
    
        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)}
    ); } }

    查看此步完整代码示例

    现在,我们可以通过点击 Square 来填充那些方格,效果与之前相同。但是,当前 state 没有保存在单个的 Square 组件中,而是保存在了 Board 组件中。每当 Board 的 state 发生变化的时候,这些 Square 组件都会重新渲染一次。把所有 Square 的 state 保存在 Board 组件中可以让我们在将来判断出游戏的胜者。

    因为 Square 组件不再持有 state,因此每次它们被点击的时候,Square 组件就会从 Board 组件中接收值,并且通知 Board 组件。在 React 术语中,我们把目前的 Square 组件称做“受控组件”。在这种情况下,Board 组件完全控制了 Square 组件。

    注意,我们调用了 .slice() 方法创建了 squares 数组的一个副本,而不是直接在现有的数组上进行修改。在下一节,我们会介绍为什么我们需要创建 square 数组的副本。

    为什么不可变性在 React 中非常重要

    在上一节内容当中,我们通过使用 .slice() 方法创建了数组的一个副本,而不是直接修改现有的数组。接下来我们来学习不可变性以及不可变性的重要性。

    一般来说,有两种改变数据的方式。第一种方式是直接修改变量的值,第二种方式是使用新的一份数据替换旧数据。

    直接修改数据

    var player = {score: 1, name: 'Jeff'};
    player.score = 2;
    // player 修改后的值为 {score: 2, name: 'Jeff'}

    新数据替换旧数据

    var player = {score: 1, name: 'Jeff'};
    
    var newPlayer = Object.assign({}, player, {score: 2});
    // player 的值没有改变, 但是 newPlayer 的值是 {score: 2, name: 'Jeff'}
    
    // 使用对象展开语法,就可以写成:
    // var newPlayer = {...player, score: 2};

    不直接修改(或改变底层数据)这种方式和前一种方式的结果是一样的,这种方式有以下几点好处:

    简化复杂的功能

    不可变性使得复杂的特性更容易实现。在后面的章节里,我们会实现一种叫做“时间旅行”的功能。“时间旅行”可以使我们回顾井字棋的历史步骤,并且可以“跳回”之前的步骤。这个功能并不是只有游戏才会用到——撤销和恢复功能在开发中是一个很常见的需求。不直接在数据上修改可以让我们追溯并复用游戏的历史记录。

    跟踪数据的改变

    如果直接修改数据,那么就很难跟踪到数据的改变。跟踪数据的改变需要可变对象可以与之改变之前的版本进行对比,这样整个对象树都需要被遍历一次。

    跟踪不可变数据的变化相对来说就容易多了。如果发现对象变成了一个新对象,那么我们就可以说对象发生改变了。

    确定在 React 中何时重新渲染

    不可变性最主要的优势在于它可以帮助我们在 React 中创建 pure components。我们可以很轻松的确定不可变数据是否发生了改变,从而确定何时对组件进行重新渲染。

    查阅性能优化章节,以了解更多有关 shouldComponentUpdate() 函数及如何构建 pure components 的内容。

    函数组件

    接下来我们把 Square 组件重写为一个函数组件

    如果你想写的组件只包含一个 render 方法,并且不包含 state,那么使用函数组件就会更简单。我们不需要定义一个继承于 React.Component 的类,我们可以定义一个函数,这个函数接收 props 作为参数,然后返回需要渲染的元素。函数组件写起来并不像 class 组件那么繁琐,很多组件都可以使用函数组件来写。

    把 Square 类替换成下面的函数:

    function Square(props) {
      return (
        
      );
    }

    我们把两个 this.props 都替换成了 props

    查看此步完整代码示例

    注意

    当我们把 Square 修改成函数组件时,我们同时也把 onClick={() => this.props.onClick()} 改成了更短的 onClick={props.onClick}(注意两侧没有括号)。

    轮流落子

    现在井字棋还有一个明显的缺陷有待完善:目前还不能在棋盘上标记 “O”。

    我们将 “X” 默认设置为先手棋。你可以通过修改 Board 组件的构造函数中的初始 state 来设置默认的第一步棋子:

    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 方法中 “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 (
           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)}
    ); } }

    查看此步完整代码示例

    判断出胜者

    至此我们就可以看出下一步会轮到哪位玩家,与此同时,我们还需要显示游戏的结果来判定游戏结束。拷贝如下 calculateWinner 函数并粘贴到文件底部:

    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”。

    接着,在 Board 组件的 render 方法中调用 calculateWinner(squares) 检查是否有玩家胜出。一旦有一方玩家胜出,就把获胜玩家的信息显示出来,比如,“胜者:X” 或者“胜者: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 (
          // 其他部分没有修改

    最后,修改 handleClick 事件,当有玩家胜出时,或者某个 Square 已经被填充时,该函数不做任何处理直接返回。

      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 的基本常识。所以坚持到这一步的你才是真正的赢家呀!

    时间旅行

    接下来是最后一个练习,我们将实现“回到过去”的功能,从而在游戏里跳回到历史步骤。

    保存历史记录

    如果我们直接修改了 square 数组,实现时间旅行就会变得很棘手了。

    不过,我们可以使用 slice() 函数为每一步创建 squares 数组的副本,同时把这个数组当作不可变对象。这样我们就可以把所有 squares 数组的历史版本都保存下来了,然后可以在历史的步骤中随意跳转。

    我们把历史的 squares 数组保存在另一个名为 history 的数组中。history 数组保存了从第一步到最后一步的所有的棋盘状态。history 数组的结构如下所示:

    history = [
      // 第一步之前
      {
        squares: [
          null, null, null,
          null, null, null,
          null, null, null,
        ]
      },
      // 第一步之后
      {
        squares: [
          null, null, null,
          null, 'X', null,
          null, null, null,
        ]
      },
      // 第二步之后
      {
        squares: [
          null, null, null,
          null, 'X', null,
          null, null, 'O',
        ]
      },
      // ...
    ]

    现在,我们需要确定应该在哪一个组件里保存 history 这个 state。

    再次提升状态

    我们希望顶层 Game 组件展示出一个历史步骤的列表。这个功能需要访问 history 的数据,因此我们把 history 这个 state 放在顶层 Game 组件中。

    我们把 history state 放在了 Game 组件中,这样就可以从它的子组件 Board 里面删除掉 square 中的 state。正如我们把 Square 组件的状态提升到 Board 组件一样,现在我们来把 state 从 Board 组件提升到顶层的 Game 组件里。这样,Game 组件就拥有了对 Board 组件数据的完全控制权,除此之外,还可以让 Game 组件控制 Board 组件,并根据 history 渲染历史步骤。

    首先,我们在 Game 组件的构造函数中初始化 state:

    class Game extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          history: [{
            squares: Array(9).fill(null),
          }],
          xIsNext: true,
        };
      }
    
      render() {
        return (
          
    {/* status */}
      {/* TODO */}
    ); } }

    下一步,我们让 Board 组件从 Game 组件中接收 squares 和 onClick 这两个 props。因为当前在 Board 组件中已经有一个对 Square 点击事件的监听函数了,所以我们需要把每一个 Square 的对应位置传递给 onClick 监听函数,这样监听函数就知道具体哪一个 Square 被点击了。以下是修改 Board 组件的几个必要步骤:

    • 删除 Board 组件中的 constructor 构造函数。
    • 把 Board 组件的 renderSquare 中的 this.state.squares[i] 替换为 this.props.squares[i]
    • 把 Board 组件的 renderSquare 中的 this.handleClick(i) 替换为 this.props.onClick(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 (
           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 (
          
    {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)}
    ); } }

    接着,更新 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 (
          
    this.handleClick(i)} />
    {status}
      {/* TODO */}
    ); }

    由于 Game 组件渲染了游戏的状态,因此我们可以将 Board 组件 render 方法中对应的代码移除。修改之后,Board 组件的 render 函数如下所示:

      render() {
        return (
          
    {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)}
    ); }

    最后,我们需要把 Board 组件的 handleClick 方法移动 Game 组件中。同时,我们也需要修改一下 handleClick 方法,因为这两个组件的 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,
        });
      }

    注意

    concat() 方法可能与你比较熟悉的 push() 方法不太一样,它并不会改变原数组,所以我们推荐使用 concat()

    到目前为止,Board 组件只需要 renderSquare 和 render 这两个方法。而游戏的状态和 handleClick 方法则会放在 Game 组件当中。

    查看此步完整代码示例

    展示历史步骤记录

    由于我们已经记录了井字棋的历史记录,因此我们可以把这些记录以历史步骤列表的形式展示给玩家。

    在前文中提到的 React 元素被视为 JavaScript 一等公民中的对象(first-class JavaScript objects),因此我们可以把 React 元素在应用程序中当作参数来传递。在 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 (
            
  • ); }); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); } return (
    this.handleClick(i)} />
    {status}
      {moves}
    ); }

    查看此步完整代码示例

    对于井字棋历史记录的每一步,我们都创建出了一个包含按钮 

  • ); });

    查看此步完整代码示例

    因为 jumpTo 还未定义,所以你点击列表项的按钮时,会出现报错。在我们实现 jumpTo 之前,我们向 Game 组件的 state 中添加 stepNumber,这个值代表我们当前正在查看哪一项历史记录。

    首先,我们在 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 方法以更新状态 stepNumber。除此之外,当状态 stepNumber 是偶数时,我们还要把 xIsNext 设为 true:

      handleClick(i) {
        // 这个方法无更改
      }
    
      jumpTo(step) {
        this.setState({
          stepNumber: step,
          xIsNext: (step % 2) === 0,
        });
      }
    
      render() {
        // 这个方法无更改
      }

    接下来,我们还要修改 Game 组件的 handleClick 方法,当你点击方格的时候触发该方法。

    新添加的 stepNumber state 用于给用户展示当前的步骤。每当我们落下一颗新棋子的时候,我们需要调用 this.setState 并传入参数 stepNumber: history.length,以更新 stepNumber。这就保证了保证每走一步 stepNumber 会跟着改变。

    我们还把读取 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);
    
        // 其他部分没有改变

    如果我们点击游戏历史记录的任何一步,井字棋的棋盘就会立即更新为刚走那一步棋时候的样子。

    查看此步完整代码示例

    总结

    恭喜你!你已经完成了一个拥有以下功能的井字棋啦:

    • tic-tac-toe(三连棋)游戏的所有功能
    • 能够判定玩家何时获胜
    • 能够记录游戏进程
    • 允许玩家查看游戏的历史记录,也可以查看任意一个历史版本的游戏棋盘状态

    干的不错!我们希望你至此已经基本掌握了 React 的使用。

    在这里可以查看最终的游戏代码:最终成果.

    如果你还有充裕的时间,或者想练习一下刚刚学会的 React 新技能,这里有一些可以改进游戏的想法供你参考,这些功能的实现顺序的难度是递增的:

    1. 在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
    2. 在历史记录列表中加粗显示当前选择的项目。
    3. 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
    4. 添加一个可以升序或降序显示历史记录的按钮。
    5. 每当有人获胜时,高亮显示连成一线的 3 颗棋子。
    6. 当无人获胜时,显示一个平局的消息。

    通过这篇教程,我们接触了 React 中的一些概念,比如 React 元素、React 组件、props,还有 state。更多关于这些概念的细节的解释,参考文档的其他部分。了解更多关于组件定义的内容,参考React.Component API reference。

    欢迎关注技术公众号:架构师成长营

    入门教程: 认识 React_第4张图片

     

    你可能感兴趣的:(前端)