react学写棋类游戏

安装

使用Create React App,最好的方式,但是必须node>6的版本
全局安装

npm install -g create-react-app
create-react-app my-app
cd my-app
npm start
  1. 删除掉生成项目中 src/ 文件夹下的所有文件。

  2. src/ 文件夹下新建一个名为 index.css 的文件并拷贝 这里的 CSS 代码 到文件中。

  3. src/ 文件夹下新建一个名为 index.js 的文件并拷贝 这里的 JS 代码 到文件中, 并在此文件的最开头加上下面几行代码:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

接下来通过命令行在你的项目目录下运行 npm start 命令并在浏览器中打开 http://localhost:3000 你就能够看到空的井字棋棋盘了

开始编码


import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';


class Square extends React.Component {
    render() {
        return (
            <button className="square">
                {/* TODO */}
            </button>
        );
    }
}

class Board extends React.Component {
    renderSquare(i) {
        return <Square />;
    }

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

class Game extends React.Component {
    render() {
        return (
            <div className="game">
                <div className="game-board">
                    <Board />
                </div>
                <div className="game-info">
                    <div>{/* status */}</div>
                    <ol>{/* TODO */}</ol>
                </div>
            </div>
        );
    }
}

// ========================================

ReactDOM.render(
    <Game />,
    document.getElementById('root')
);

讲得更具体一点,我们现在有3个组件:

Square
Board
Game
Square 组件代表一个单独的 <button>,Board 组件包含了9个squares,也就是棋盘的9个格子。Game 组件则为我们即将要编写的代码预留了一些位置。现在这几个组件都是不具备任何的交互功能的

通过 Props 传递数据

我们先来试着从 Board 组件传递一些数据到 Square 组件。

在 Board 组件的 renderSquare 方法中,我们将代码改写成下面这样,传递一个名为 value 的 prop 到 Square 当中

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />;
  }
class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}
      </button>
    );
  }
}
image.png

给组件添加交互功能

接下来我们试着让棋盘的每一个格子在点击之后能落下一颗 “X” 作为棋子。我们试着把 render() 方法修改为如下内容:
现在你试着点击一下某个格子,在浏览器里就会弹出一个警示框。
在 React 组件的构造方法 constructor 当中,你可以通过 this.state 为该组件设置自身的状态数据。我们来试着把棋盘格子变化的数据储存在组件的 state 当中吧:

首先,我们为组件添加构造函数并初始化 state:

lass Square extends React.Component {
    constructor(){
        super()
        this.state = {
            value:null,
        }
    },
    render() {
        return (
            <button className="square" onClick={()=>alert('click')}>
                {this.props.value}
            </button>
        );
    }
}

现在我们试着通过点击事件触发 state 的改变来更新棋盘格子显示的内容:

将 <button> 当中的 this.props.value 替换为 this.state.value 。
将 () => alert() 方法替换为 () => this.setState({value: 'X'}) 。
现在我们的 <button> 标签就变成了下面这样:

class Square extends React.Component {
    constructor(){
        super()
        this.state = {
            value:null,
        }
    }
    render() {
        return (
            <button className="square" onClick={()=>this.setState({value: 'X'})}>
                {this.state.value}
            </button>
        );
    }
}

每当 this.setState 方法被触发时,组件都会开始准备更新,React 通过比较状态的变化来更新组件当中跟随数据改变了的内容。当组件重新渲染时,this.state.value 会变成 'X' ,所以你也就能够在格子里看到 X 的字样。

现在你试着点击任何一个格子,都能够看到 X 出现在格子当中。

开发工具

ChromeFirefox 上安装 React 开发者工具可以让你在浏览器的开发控制台里看到 React 渲染出来的组件树。
你同样可以在开发工具中观察到各个组件的 props 和 state.
安装好开发工具之后,你可以在任意页面元素上面右键选择 “审查元素”,之后在弹出的控制台选项卡最右边会看到名为 React 的选项卡。

状态提升

我们现在已经编写好了井字棋游戏最基本的可以落子的棋盘。但是现在应用的状态是独立保存在棋盘上每个格子的 Square 组件当中的。想要编写出来一个真正能玩的游戏,我们还需要判断哪个玩家获胜,并在 X 或 O 两方之间交替落子。想要检查某个玩家是否获胜,需要获取所有9个格子上面的棋子分布的数据,现在这些数据分散在各个格子当中显然是很麻烦的。
最好的解决方式是直接将所有的 state 状态数据存储在 Board 组件当中。之后 Board 组件可以将这些数据传递给各个 Square 组件
当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中的状态数据就能够更方便地交流共享了
像这种提升组件状态的情形在重构 React 组件时经常会遇到。我们趁现在也就来实践一下,在 Board 组件的构造函数中初始化一个包含9个空值的数组作为状态数据,并将这个数组中的9个元素分别传递到对应的9个 Square 组件当中。

 constructor(){
        super()
        this.state = {
            squartes: Array(9).fill(null)
        }
    }

现在传入的都是空数据,井字棋游戏进行会把数组填充成类似下面这样:

[
  'O', null, 'X',
  'X', 'X', 'O',
  'O', null, null,
]

我们在 value 属性中传递对应 state 数组元素的值。

    renderSquare(i) {
        return <Square value={this.state.squares[i]}/>;
    }

现在我们需要修改当某个格子被点击时触发的事件处理函数。现在每个格子当中的数据是存储在整个棋盘当中的,所以我们就需要通过一些方法,让格子组件能够修改整个棋盘组件数据的内容。因为每个组件的 state 都是它私有的,所以我们不可以直接在格子组件当中进行修改。

惯例的做法是,我们再通过 props 传递一个父组件当中的事件处理函数到子组件当中。也就是从 Board 组件里传递一个事件处理函数到 Square 当中,我们来把 renderSquare 方法改成下面这样


    renderSquare(i) {
        return <Square
                  value={this.state.squares[i]}
                  onClick={()=>this.handleClick(i)}
                />;
    }

注意到我们在写代码的时候,在各个属性直接换了行,这样可以改善我们代码的可读性。并且我们在 JSX 元素的最外层套上了一小括号,以防止 JavaScript 代码在解析时自动在换行处添加分号。

现在我们从 Board 组件向 Square 组件中传递两个 props 参数:value 和 onClick. onClick 里传递的是一个之后在 Square 组件中能够触发的方法函数。我们动手来修改代码吧:

将 Square 组件的 render 方法中的 this.state.value 替换为 this.props.value 。
将 Square 组件的 render 方法中的 this.setState() 替换为 this.props.onClick() 。
删掉 Square 组件中的 构造函数 constructor ,因为它现在已经不需要保存 state 了。
进行如上修改之后,代码会变成下面这样:

class Square extends React.Component {
  render() {
    return (
      <button className="square" onClick={() => this.props.onClick()}>
        {this.props.value}
      </button>
    );
  }
}

现在每次格子被点击时就会触发传入的 onClick 方法。我们来捋一下这其中发生了什么:

1.添加 onClick 属性到内置的 DOM 元素 <button> 上让 React 开启了对点击事件的监听。
2.当按钮,也就是棋盘格子被点击时, React 会调用 Square 组件的 render() 方法中的 onClick 事件处理函数。
3.事件处理函数触发了传入其中的 this.props.onClick() 方法。这个方法是由 Board 传递给 Square 的。
4.Board 传递了 onClick={() => this.handleClick(i)} 给 Square,所以当 Square 中的事件处理函数触发时,其实就是触发的 Board 当中的 this.handleClick(i) 方法。
5.现在我们还没有编写 handleClick() 方法,所以代码还不能正常工作。
注意到这里的 onClick 事件是 React 组件当中所特有的。不过 handleClick 这些方法则只是我们编写事件处理函数时候的命名习惯。

现在我们来动手编写 handleClick 方法吧:

handleClick(i) {
        //slice() 方法可从已有的数组中返回选定的元素 创建和state.squares一样的数组
       const squares = this.state.squares.slice()
        squares[i] = 'X'
        this.setState({squares: squares})
    }

我们使用了 .slice() 方法来将之前的数组数据浅拷贝到了一个新的数组中,而不是修改已有的数组。你可以在 这个章节 来了解为什么不可变性在 React 当中的重要性。

现在你点击棋盘上的格子应该就能够正常落子了。而且状态数据是统一保管在棋盘组件 Board 当中的。你应该注意到了,当事件处理函数触发棋盘父组件的状态数据改变时,格子子组件会自动重新渲染。

现在格子组件 Square 不再拥有自身的状态数据了。它从棋盘父组件 Board 接受数据,并且当自己被点击时通知触发父组件改变状态数据,我们称这类的组件为 受控组件

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

在上一节内容当中,我们通过使用 .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'}

// 或者使用最新的对象分隔符语法,你可以这么写:
// var newPlayer = {...player, score: 2};

函数定义组件

我们刚才已经去掉了 Square 的构造函数,事实上,更进一步的,React 专门为像 Square 组件这种只有 render 方法的组件提供了一种更简便的定义组件的方法: 函数定义组件 。只需要简单写一个以 props 为参数的 function 返回 JSX 元素就搞定了。

下面我们以函数定义的方式重写 Square 组件:

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

记得把所有的 this.props 替换成参数 props. 我们应用中的大部分简单组件都可以通过函数定义的方式来编写,并且 React 在将来还会对函数定义组件做出更多优化。

另外一部分简化的内容则是事件处理函数的写法,这里我们把 onClick={() => props.onClick()} 直接修改为 onClick={props.onClick} , 注意不能写成 onClick={props.onClick()} 否则 props.onClick 方法会在 Square 组件渲染时被直接触发而不是等到 Board 组件渲染完成时通过点击触发,又因为此时 Board 组件正在渲染中(即 Board 组件的 render() 方法正在调用),又触发 handleClick(i) 方法调用 setState() 会再次调用 render() 方法导致死循环。

轮流落子

很明显现在我们点击棋盘只后落子的只有 X 。 下面我们要开发出 X 和 O 轮流落子的功能。

我们将 X 默认设置为先手棋:

class Board extends React.Component {
  constructor() {
    super();
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

接下来,我们每走一步棋,都需要切换 xIsNext 的值以此来实现轮流落子的功能,接下来在 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 轮流落子的效果了。我们再到 render 方法里添加一点内容来显示当前执子的一方

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

    return (
      // the rest has not changed

判断赢家

接下来我们来编写判断游戏获胜方的代码,首先在你的代码里添加下面这个判断获胜方的算法函数

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

然后你就可以在 Board 组件的 render 方法里调用它,来检查是否有人获胜并根据判断显示出 “Winner: [X/O]” 来表示获胜方。

将 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 (
      // the rest has not changed

继续完善游戏规则,我们在 handleClick 里添加当前方格内已经落子/有一方获胜就就无法继续落子的判断逻辑

 handleClick(i) {
       const squares = this.state.squares.slice()
        if(this.calculateWinner(squares) || squares[i]) return
        squares[i] = this.state.xIsNext ? 'X' : 'O'
        this.setState({
            squares: squares,
            xIsNext: !this.state.xIsNext,
        })
    }

保存历史记录

接下来我们一起来实现保存棋局每一步的历史记录的功能。在现有的代码逻辑中,我们已经是在每走一步棋之后就返回一个新的 squares 数组了,所以想要保存历史记录也非常简单。

我们计划通过一个数组对象来保存每一步的状态数据

我们期望在顶层的 Game 组件中展示一个链接每一步历史记录的列表。所以就像我们之前将 state 从 Square 组件提升到 Board 中一样,现在我们把 Board 中的状态数据再提升到 Game 组件中来。

首先在 Game 组件的构造函数中初始化我们需要的状态数据

class Game extends React.Component {
    constructor(){
        super()
        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>
        );
    }
}

接下来,就好像我们之前对 Square 组件的操作一样。我们将 Board 中的状态数据全都移动到 Game 组件当中。Board 现在通过 props 获取从 Game 传递下来的数据和事件处理函数。

1.删除 Board 的构造方法 constructor 。
2.把 Board 的 renderSquare 方法中的 this.state.squares[i] 替换为 this.props.squares[i] 。
3.把 Board 的 renderSquare 方法中的 this.handleClick(i) 替换为 this.props.onClick(i) 。

将calculateWinner提到外面公用的

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 方法里的 <div className="status">{status}</div> 删掉:

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

之后,我们需要将 Board 组件里的 handleClick 移动到 Game 组件当中。你可以直接把它剪切粘贴过来。

不过为了实现我们新的历史记录的功能,还需要稍微修改一下我们的代码,让 handleClick 在每次触发时,添加当前的棋局状态数据到 histroy 当中

 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 和 render 两个方法;应用状态 state 以及事件处理函数现在都定义在 Game 组件当中。
完成代码


import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';


function Square(props) {
    return (
        <button className="square" onClick={props.onClick}>
            {props.value}
        </button>
    )
}

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

class Board extends React.Component {

    renderSquare(i) {
        return <Square
                  value={this.props.squares[i]}
                  onClick={()=>this.props.onClick(i)}
                />;
    }

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

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

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

    }


    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 = 'Nest 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>
        );
    }
}

ReactDOM.render(
    <Game />,
    document.getElementById('root')
);

展示每步历史记录链接

现在我们来试着展示每一步棋的历史记录链接。在教程的开始我们提到过,React 元素事实上都是 JS 当中的对象,我们可以把元素当作参数或定义到变量中使用。在 React 当中渲染多个重复的项目时,我们一般都以数组的方式传递 React 元素。最基本的方法是使用数组的 map 方法,我们试着来修改 Game 组件的 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 ?
        'Move #' + move :
        'Game start';
      return (
        <li>
          <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
        </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>
    );
  }

对于每一步的历史记录,我们都创建了一个带 <a> 链接的 <li> 列表项。目前链接还没指向任何地方,别着急我们后面会继续实现切换至对应棋步的功能。有了我们现有的代码,已经能渲染出一个列表了,不过你留心的话,就会在控制台看到警告:

Keys

当你在 React 当中渲染列表项时,React 都会试着存储对应每个单独项的相关信息。如果你的组件包含 state 状态数据,那么这些状态数据必须被排序,不管你组件是怎么编写实现的。

当你想要更新这些列表项时,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 被移到了最后,多出来一个 Claudia。可是 React 只是电脑里运行地程序,它无从知晓这些改变。所以我们必须为列表中的每一项添加一个 key 作为唯一的标识符。标识符必须是唯一的,比方说刚才这个例子中的 alexa, ben, claudia 就可以用来做标识符。更普遍的一种情况,假如我们的数据是从数据库获取的话,表单每一项的 ID 就很适合当作它的 key :

<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>

强烈建议你在渲染列表项时添加 keys 值。 假如你没有现成可以作为唯一 key 值的数据使用的话,你可能需要考虑重新组织设计你的数据了。

实现时间旅行

在我们的棋步的列表中,已经有了现成的唯一 key 值,也就是每一次 move 的记录值。我们通过 <li key={move}> 来添加一下。

 const moves = history.map((step, move) => {
      const desc = move ?
        'Move #' + move :
        'Game start';
      return (
        <li key={move}>
          <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
        </li>
      );
    });

在上面的代码中,我们同样为每一个 <a> 添加了一个 jumpTo 方法,用来将棋盘的状态切换至对应的棋步时的状态。接下来我们来着手实现这个方法:

首先在 Game 组件的初始状态中多设置一项 stepNumber: 0

class Game extends React.Component {
  constructor() {
    super();
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
    };
  }

接下来,我们正是编写 jumpTo 来切换 stepNumber 的值。根据游戏的逻辑,与此同时我们还需要修改 xIsNext 来保证对应棋步时,执子的一方是能对应上的。我们可以根据棋步计算出是谁在执子。

我们把 jumpTo 编写在 Game 组件中:

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) ? false : true,
    });
  }

接下来,我们在 handleClick 方法中对 stepNumber 进行更新,添加 stepNumber: history.length 保证每走一步 stepNumber 会跟着改变:

 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 方法里根据当前的棋步获取对应的棋局状态了:

render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

查看此步完整代码示例。

现在你试着点击每一步棋记录的列表中的一项,棋盘会自动更新到对应项时的棋局状态

总结,招帮官网文档来的,算是自我学习的一种方式

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341