React学习——井字游戏(官网教程笔记)

一、安装node

  1. 可以通过官网下载稳定版本并执行安装;
  2. 或者通过nvm安装你需要的node版本(可以对node版本进行管理,比较推荐)。

二、创建新项目

  1. 打开命令终端;

  2. 进入你要存放项目的路径;

  3. 创建react项目"tic-tac-toe"(项目名称):

    npx create-react-app tic-tac-toe
    
  4. 进入项目"tic-tac-toe":

    cd tic-tac-toe
    
  5. 运行项目:

    run start
    
  6. 浏览器访问地址http://localhost:3000,可看到默认页面;

  7. 使用IDE或者编辑器打开项目(推荐使用 webstorm、vscode ),列表如下,已忽略"node_modules":

    │  .gitignore
    │  package-lock.json
    │  package.json
    │  README.md
    │          
    ├─public
    │      favicon.ico
    │      index.html
    │      logo192.png
    │      logo512.png
    │      manifest.json
    │      robots.txt
    │      
    └─src
            App.css
            App.js
            App.test.js
            index.css
            index.js
            logo.svg
            serviceWorker.js
            setupTests.js
    
  1. 删除src目录下的所有文件;

三、初始化项目文件

  1. 在src目录下创建index.css,可以自己编写css样式代码,也可以将例子中的样式代码复制到此文件中;

    body {
      font: 14px "Century Gothic", Futura, sans-serif;
      margin: 20px;
    }
    
    ol, ul {
      padding-left: 30px;
    }
    
    li {
      list-style: none;
    }
    
    .board-row:after {
      clear: both;
      content: "";
      display: table;
    }
    
    .status {
      margin-bottom: 10px;
    }
    
    .square {
      background: #fff;
      border: 1px solid #999;
      float: left;
      font-size: 24px;
      font-weight: bold;
      height: 60px;
      margin-right: -1px;
      margin-top: -1px;
      padding: 0;
      text-align: center;
      width: 60px;
    }
    
    .square:focus {
      outline: none;
    }
    
    .kbd-navigation .square:focus {
      background: #ddd;
    }
    
    .game {
      display: flex;
      flex-direction: row;
      justify-content: center;
    }
    
    .game-info {
      margin-left: 20px;
    }
    
  2. 在src目录下创建index.js,引入所需要的依赖:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    
  3. 将例子中的javascript代码复制到index.js文件中;

    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')
    );
    
  4. 再次查看浏览器,此时应该已经刷新页面,或者可以手动刷新;

四、改造代码

  1. Board组件中向Square组件传递参数,value={i}

      renderSquare(i) {
        return <Square value={i} />;
      }
    
  2. Square组件中接收Board组件传来的参数,并显示:

    class Square extends React.Component {
      render() {
        return (
          <button className="square">
            {this.props.value}
          </button>
        );
      }
    }
    
  3. 尝试在Square组件上添加点击方法:

    class Square extends React.Component {
      render() {
        return (
          <button className="square" onClick={() => {
            console.log(this.props.value);}}>
            {this.props.value}
          </button>
        );
      }
    }
    
  4. Square组件中添加状态用来记录按钮被点击过:

    class Square extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          value: null
        };
      }
      render() {
        return (
          <button className="square" onClick={() => {
            console.log(this.props.value);}}>
            {this.props.value}
          </button>
        );
      }
    }
    
  5. 通过setState()方法,当点击按钮,就把按钮的显示内容改为state.value的值:

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

五、进一步完善,轮流落子

  1. Square组件中的状态统一移动到父组件Board中进行管理:

    class Board extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          squares: Array(9).fill(null)
        }
      }
    }
    
  2. 在父组件Board中向子组件Square传递两个参数valueonClick

    class Board extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          squares: Array(9).fill(null)
        }
      }
      renderSquare(i) {
        return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />;
      }
    }
    
  3. 在子组件Square修改按钮显示为父组件传来的参数,监听的点击方法也为父组件传来的方法:

    class Square extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          value: null
        };
      }
      render() {
        return (
          <button
            className="square"
            onClick={() => {this.props.onClick()}}>
            {this.props.value}
          </button>
        );
      }
    }
    
  4. 删除子组件Square的自有状态:

    class Square extends React.Component {
      render() {
        return (
          <button
            className="square"
            onClick={() => {this.props.onClick()}}>
            {this.props.value}
          </button>
        );
      }
    }
    
  5. 优化子组件Square为函数组件,无状态使用函数组件更效率:

    function Square(props) {
      return (
        <button
          className="square"
          onClick={props.onClick}>
          {props.value}
        </button>
      );
    }
    
  6. 定义父组件Board的handleClick方法:

      handleClick(i) {
        let temp = this.state.squares.slice();
        temp[i] = "X";
        this.setState({
          squares: temp
        });
      }
    
  7. 完善轮流落子功能,在父组件Board中设置一个开关isX,默认为true,每落下一步,isX将反转,依此判断下一步该落什么子:

     constructor(props) {
       super(props);
       this.state = {
         squares: Array(9).fill(null),
         isX: true
       }
     }  
     handleClick(i) {
       let temp = this.state.squares.slice();
       temp[i] = this.state.isX ? "X" : "O";
       this.setState({
         squares: temp,
         isX: !this.state.isX
       });
     }
    
  8. 在父组件Board中在修改提示信息,以便知道下一步是什么子:

    const status = `Next player: ${this.state.isX ? "X" : "O"}`;
    

六、进一步完善,判断输赢

  1. 编写判断输赢的方法,放在index.js最后面,写好输赢规则lines,依次遍历参数lines,将squares中每一项进行三三比对,得出结果:

    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;
    }
    
  2. 父组件Board中,修改点击事件,当已经有人胜出,或者当前方格已经有数据,不做操作:

      handleClick(i) {
        let temp = this.state.squares.slice();
        if (calculateWinner(this.state.squares) || temp[i]) {
          return;
        }
        temp[i] = this.state.isX ? "X" : "O";
        this.setState({
          squares: temp,
          isX: !this.state.isX
        });
      }
    

七、继续完善,增加历史记录功能

  1. 在顶层Game组件中添加状态history,用来保存每一步的棋盘数据,并且把Board组件中的状态移到Game组件中:

    class Game extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          history: [{
            squares: Array(9).fill(null)
          }],
          isX: true
        }
      }
    }
    
  2. Board组件中的状态移除,并把原先状态中的参数改为接收父组件Game传来的参数:

    class Board extends React.Component {
      renderSquare(i) {
        return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />;
      }
      render() {
        return (
          <div>
            <div className="status">{this.props.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>
        );
      }
    }
    
  3. Board组件中的handleClick方法移动到父组件Game中,根据当前Game组件的状态进行调整:

    class Game extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          history: [{
            squares: Array(9).fill(null)
          }],
          isX: true
        }
      }
      handleClick(i) {
        const history = this.state.history;
        const cur = history[history.length -1];
        let temp = cur.squares.slice();
        if (calculateWinner(temp) || temp[i]) {
          return;
        }
        temp[i] = this.state.isX ? "X" : "O";
        this.setState({
          history: history.concat([{
            squares: temp
          }]),
          isX: !this.state.isX
        });
      }
    }
    
  4. Board组件中render方法中的前置判断移动到父组件Game中,并在调用Board组件的时候传入参数squaresstatusonClick

    class Game extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          history: [{
            squares: Array(9).fill(null)
          }],
          isX: true
        }
      }
      handleClick(i) {
        const history = this.state.history;
        const cur = history[history.length -1]; // 使用最新一次的棋盘数据
        let temp = cur.squares.slice();
        if (calculateWinner(temp) || temp[i]) {
          return;
        }
        temp[i] = this.state.isX ? "X" : "O";
        this.setState({
          history: history.concat([{
            squares: temp
          }]),
          isX: !this.state.isX
        });
      }
      render() {
        const history = this.state.history;
        const cur = history[history.length -1]; // 使用最新一次的棋盘数据
        const winner = calculateWinner(cur.squares);
        let status = "";
        if (winner) {
          status = `Winner is: ${winner}`;
        } else {
          status = `Next player: ${this.state.isX ? "X" : "O"}`;
        }
    
        return (
          <div className="game">
            <div className="game-board">
              <Board
                squares={cur.squares}
                status={status}
                onClick={i => this.handleClick(i)}
              />
            </div>
            <div className="game-info">
              <div>{/* status */}</div>
              <ol>{/* TODO */}</ol>
            </div>
          </div>
        );
      }
    }
    

八、展示历史记录

  1. Game中渲染一个历史记录按钮列表,使用history状态的map方法形成一个history的历史记录映射:

        const moves = history.map((item, i) => {
          const con = i ? `Go to step-${i}` : `Go to start`;
          return(
            <li>
              <button onClick={() => this.jumpTo(i)}>{con}</button>
            </li>
          )
        });
    
  2. 给每一步添加唯一key:

        const moves = history.map((item, i) => {
          const con = i ? `Go to step-${i}` : `Go to start`;
          return(
            <li key={`moves${Math.floor(Math.random() * 900000 + 100000)}`}>
              <button onClick={() => this.jumpTo(i)}>{con}</button>
            </li>
          )
        });
    
  3. Game中将moves映射插入到对应展示的位置:

        return (
          <div className="game">
            <div className="game-board">
              <Board
                squares={cur.squares}
                status={status}
                onClick={i => this.handleClick(i)}
              />
            </div>
            <div className="game-info">
              <div>{status}</div>
              <ol>{moves}</ol>
            </div>
          </div>
        )
    
  4. Game中声明状态参数step,来表示当前步骤:

      constructor(props) {
        super(props);
        this.state = {
          history: [{
            squares: Array(9).fill(null)
          }],
          isX: true,
          step: 0
        }
      }
    
  5. Game中编写jumpTo方法,更改step状态,并且当步数是偶数时,isXtrue

      jumpTo(i) {
        this.setState({
          step: i,
          isX: i % 2 === 0
        })
      }
    
  6. Game中修改handleClick方法,每走一步,同时更新step状态:

      handleClick(i) {
        const history = this.state.history;
        const cur = history[history.length -1];
        let temp = cur.squares.slice();
        if (calculateWinner(temp) || temp[i]) {
          return;
        }
        temp[i] = this.state.isX ? "X" : "O";
        this.setState({
          history: history.concat([{
            squares: temp
          }]),
          isX: !this.state.isX,
          step: history.length
        });
      }
    
  7. Game中修改handleClick方法,将history改成截取0到当前步骤的数据:

      handleClick(i) {
        const history = this.state.history.slice(0, this.state.step + 1);
        const cur = history[history.length -1];
        let temp = cur.squares.slice();
        if (calculateWinner(temp) || temp[i]) {
          return;
        }
        temp[i] = this.state.isX ? "X" : "O";
        this.setState({
          history: history.concat([{
            squares: temp
          }]),
          isX: !this.state.isX,
          step: history.length
        });
      }
    
  8. Game中修改render方法中当前棋盘通过当前步骤获取:

        const history = this.state.history;
        const cur = history[this.state.step]; //通过当前步骤获取 
        const winner = calculateWinner(cur.squares);
    
  9. 完成

九、功能优化

  1. 在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号):

    class Board extends React.Component {
      renderSquare(i, axis) {
        /* 接收坐标参数 */
        return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i, axis)} />; 
      }
      render() {
        return (
          <div>
            <div className="status">{this.props.status}</div>
            <div className="board-row">
              {this.renderSquare(0, [1,1])} // 传入坐标参数
              {this.renderSquare(1, [1,2])}
              {this.renderSquare(2, [1,3])}
            </div>
            <div className="board-row">
              {this.renderSquare(3, [2,1])}
              {this.renderSquare(4, [2,2])}
              {this.renderSquare(5, [2,3])}
            </div>
            <div className="board-row">
              {this.renderSquare(6, [3,1])}
              {this.renderSquare(7, [3,2])}
              {this.renderSquare(8, [3,3])}
            </div>
          </div>
        );
      }
    }
    
    class Game extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          history: [{
            squares: Array(9).fill(null),
            axis: [] // 声明坐标状态
          }],
          isX: true,
          step: 0
        }
      }
    
      handleClick(i, axis) { // 接收坐标参数
        const history = this.state.history.slice(0, this.state.step + 1);
        const cur = history[history.length - 1];
        let temp = cur.squares.slice();
        if (calculateWinner(temp) || temp[i]) {
          return;
        }
        temp[i] = this.state.isX ? "X" : "O";
        this.setState({
          history: history.concat([{
            squares: temp,
            axis: axis // 更改坐标状态
          }]),
          isX: !this.state.isX,
          step: history.length,
        });
      }
    
      jumpTo(i) {
        this.setState({
          step: i,
          isX: (i % 2) === 0
        });
      }
    
      render() {
        const history = this.state.history;
        const cur = history[this.state.step];
        const winner = calculateWinner(cur.squares);
    
        const moves = history.map((item, i) => {
          // 展示坐标
          const con = i ? `Go to step-${i}-[${item.axis[0]},${item.axis[1]}]` : `Go to Game start`;
          const randomKey = `moves${Math.floor(Math.random() * 900000 + 100000)}`;
          return(
            <li key={randomKey}>
              <button onClick={() => this.jumpTo(i)} className={}>{con}</button>
            </li>
          )
        });
    
        let status = "";
        if (winner) {
          status = `Winner is: ${winner}`;
        } else {
          status = `Next player: ${this.state.isX ? "X" : "O"}`;
        }
    
        return (
          <div className="game">
            <div className="game-board">
              <Board
                squares={cur.squares}
                status={status}
                onClick={(i, axis) => this.handleClick(i,axis)} // 接收、传入坐标参数
              />
            </div>
            <div className="game-info">
              <div>{status}</div>
              <ol>{moves}</ol>
            </div>
          </div>
        );
      }
    }
    
  2. 在历史记录列表中加粗显示当前选择的项目:

          return(
            // 添加判断是否需要".active"
            <li key={randomKey}>
              <button onClick={() => this.jumpTo(i)} className={this.state.step === i ? "active": ""}>{con}</button>
            </li>
          )
    
    /* index.css 添加.active */
    .active {
      font-weight: bold;
      color: #38ccff;
    }
    
  3. 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode):

    class Board extends React.Component {
      renderSquare(i, axis) {
        const randomKey = `cell${Math.floor(Math.random() * 900000 + 100000)}`;
        return <Square key={randomKey} value={this.props.squares[i]} onClick={() => this.props.onClick(i, axis)} />;
      }
      // 渲染行方法
      renderRow(j) {
        const rowACell = this.props.rowACell;
        let temp = [];
        for (let i = 0; i < rowACell; i++) {
          temp.push(this.renderSquare(i+rowACell*j, [j+1,i+1]));
        }
        const randomKey = `row${Math.floor(Math.random() * 900000 + 100000)}`;
        return(
          <div className="board-row" key={randomKey}>
            {temp}
          </div>
        )
      }
      // 渲染棋盘方法
      renderBoard() {
        const rowACell = this.props.rowACell;
        let temp = [];
        for (let i = 0; i < rowACell; i++) {
          temp.push(this.renderRow(i));
        }
        return temp;
      }
    
      render() {
        return (
          <div>
            <div className="status">{this.props.status}</div>
            {this.renderBoard()} // 渲染棋盘
          </div>
        );
      }
    }
    
    class Game extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          history: [{
            squares: Array(9).fill(null),
            axis: []
          }],
          isX: true,
          step: 0,
          rowACell: 3 // 3*3棋盘,如需可变,需要修改相应的calculateWinner()方法规则
        }
      }
    
      handleClick(i, axis) {
        const history = this.state.history.slice(0, this.state.step + 1);
        const cur = history[history.length - 1];
        let temp = cur.squares.slice();
        if (calculateWinner(temp) || temp[i]) {
          return;
        }
        temp[i] = this.state.isX ? "X" : "O";
        this.setState({
          history: history.concat([{
            squares: temp,
            axis: axis
          }]),
          isX: !this.state.isX,
          step: history.length,
        });
      }
    
      jumpTo(i) {
        this.setState({
          step: i,
          isX: (i % 2) === 0
        });
      }
    
      render() {
        const history = this.state.history;
        const cur = history[this.state.step];
        const winner = calculateWinner(cur.squares);
    
        const moves = history.map((item, i) => {
          const con = i ? `Go to step-${i}-[${item.axis[0]},${item.axis[1]}]` : `Go to Game start`;
          const randomKey = `moves${Math.floor(Math.random() * 900000 + 100000)}`;
          return(
            <li key={randomKey}>
              <button onClick={() => this.jumpTo(i)} className={this.state.step === i ? "active": ""}>{con}</button>
            </li>
          )
        });
    
        let status = "";
        if (winner) {
          status = `Winner is: ${winner}`;
        } else {
          status = `Next player: ${this.state.isX ? "X" : "O"}`;
        }
    
        return (
          <div className="game">
            <div className="game-board">
              <Board
                squares={cur.squares}
                status={status}
                rowACell={this.state.rowACell} // 传入棋盘格数
                onClick={(i, axis) => this.handleClick(i,axis)}
              />
            </div>
            <div className="game-info">
              <div>{status}</div>
              <ol>{moves}</ol>
            </div>
          </div>
        );
      }
    }
    
  4. 添加一个可以升序或降序显示历史记录的按钮:

    class Game extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          history: [{
            squares: Array(9).fill(null),
            axis: []
          }],
          isX: true,
          step: 0,
          rowACell: 3, // 3*3棋盘,如需可变,需要修改相应的calculateWinner()方法规则
          order: true // 升序true,降序false
        }
      }
    
      handleClick(i, axis) {
        const history = this.state.history.slice(0, this.state.step + 1);
        const cur = history[history.length - 1];
        let temp = cur.squares.slice();
        if (calculateWinner(temp) || temp[i]) {
          return;
        }
        temp[i] = this.state.isX ? "X" : "O";
        this.setState({
          history: history.concat([{
            squares: temp,
            axis: axis
          }]),
          isX: !this.state.isX,
          step: history.length
        });
      }
    
      jumpTo(i) {
        this.setState({
          step: i,
          isX: (i % 2) === 0
        });
      }
      
      // 改变排序方法
      changeOrder() {
        this.setState({
          order: !this.state.order
        })
      }
      
      // 最终展示的历史记录
      historyCoder(moves) {
        if (this.state.order) {
          return moves;
        } else {
          return moves.reverse();
        }
      }
    
      render() {
        const history = this.state.history;
        const cur = history[this.state.step];
        const winner = calculateWinner(cur.squares);
    
        const moves = history.map((item, i) => {
          const con = i ? `Go to step-${i}-[${item.axis[0]},${item.axis[1]}]` : `Go to Game start`;
          const randomKey = `moves${Math.floor(Math.random() * 900000 + 100000)}`;
          return(
            <li key={randomKey}>
              <button onClick={() => this.jumpTo(i)} className={this.state.step === i ? "active": ""}>{con}</button>
            </li>
          )
        });
    
        let status = "";
        if (winner) {
          status = `Winner is: ${winner}`;
        } else {
          status = `Next player: ${this.state.isX ? "X" : "O"}`;
        }
    
        return (
          <div className="game">
            <div className="game-board">
              <Board
                squares={cur.squares}
                status={status}
                rowACell={this.state.rowACell}
                onClick={(i, axis) => this.handleClick(i,axis)}
              />
            </div>
            <div className="game-info">
              <div>
                // 改变按钮文字
                <button onClick={() => this.changeOrder()}>{this.state.order ? "升序": "降序"}</button>
              </div>
           // 展示历史记录
              <ol>{this.historyCoder(moves)}</ol> 
            </div>
          </div>
        );
      }
    }
    
  5. 每当有人获胜时,高亮显示连成一线的 3 颗棋子:

    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 {
            player: squares[a],
            line: lines[i]
          };
        }
      }
      // 返回值改造
      return {
        player: null,
        line: []
      };
    }
    
    class Game extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          history: [{
            squares: Array(9).fill(null),
            axis: []
          }],
          isX: true,
          step: 0,
          rowACell: 3, // 3*3棋盘,如需可变,需要修改相应的calculateWinner()方法规则
          order: true, // 升序true,降序false
        }
      }
    
      handleClick(i, axis) {
        const history = this.state.history.slice(0, this.state.step + 1);
        const cur = history[history.length - 1];
        let temp = cur.squares.slice();
        // 修改winner
        const res = calculateWinner(temp);
        const winner = res.player;
        if (winner || temp[i]) {
          return;
        }
        temp[i] = this.state.isX ? "X" : "O";
        this.setState({
          history: history.concat([{
            squares: temp,
            axis: axis
          }]),
          isX: !this.state.isX,
          step: history.length
        });
      }
    
      jumpTo(i) {
        this.setState({
          step: i,
          isX: (i % 2) === 0
        });
      }
    
      changeOrder() {
        this.setState({
          order: !this.state.order
        })
      }
    
      historyCoder(moves) {
        if (this.state.order) {
          return moves;
        } else {
          return moves.reverse();
        }
      }
    
      render() {
        const history = this.state.history;
        const cur = history[this.state.step];
        // 修改winner
        const res = calculateWinner(cur.squares);
        const winner = res.player;
    
        const moves = history.map((item, i) => {
          const con = i ? `Go to step-${i}-[${item.axis[0]},${item.axis[1]}]` : `Go to Game start`;
          const randomKey = `moves${Math.floor(Math.random() * 900000 + 100000)}`;
          return(
            <li key={randomKey}>
              <button onClick={() => this.jumpTo(i)} className={this.state.step === i ? "active": ""}>{con}</button>
            </li>
          )
        });
    
        let status = "";
        if (winner) {
          status = `Winner is: ${winner}`;
        } else {
          status = `Next player: ${this.state.isX ? "X" : "O"}`;
        }
        return (
          <div className="game">
            <div className="game-board">
              <Board
                squares={cur.squares}
                status={status}
                rowACell={this.state.rowACell}
                highLight={res.line} // 传入高亮显示的参数
                onClick={(i, axis) => this.handleClick(i,axis)}
              />
            </div>
            <div className="game-info">
              <div>
                <button onClick={() => this.changeOrder()}>{this.state.order ? "升序": "降序"}</button>
              </div>
              <ol>{this.historyCoder(moves)}</ol>
            </div>
          </div>
        );
      }
    }
    
    class Board extends React.Component {
      renderSquare(i, axis) {
        // 判断是否高亮
        const highLight = this.props.highLight;
        const isHigh = highLight.includes(i);
        const randomKey = `cell${Math.floor(Math.random() * 900000 + 100000)}`;
        return <Square key={randomKey} value={this.props.squares[i]} onClick={() => this.props.onClick(i, axis)} isHigh={isHigh} />; // 传入高亮
      }
    
      renderRow(j) {
        const rowACell = this.props.rowACell;
        let temp = [];
        for (let i = 0; i < rowACell; i++) {
          temp.push(this.renderSquare(i+rowACell*j, [j+1,i+1]));
        }
        const randomKey = `row${Math.floor(Math.random() * 900000 + 100000)}`;
        return(
          <div className="board-row" key={randomKey}>
            {temp}
          </div>
        )
      }
    
      renderBoard() {
        const rowACell = this.props.rowACell;
        let temp = [];
        for (let i = 0; i < rowACell; i++) {
          temp.push(this.renderRow(i));
        }
        return temp;
      }
    
      render() {
        return (
          <div>
            <div className="status">{this.props.status}</div>
            {this.renderBoard()}
          </div>
        );
      }
    }
    
    function Square(props) {
      return (
        <button
          className="square"
          onClick={props.onClick}
          style={{color: props.isHigh ? "red": "black"}} // 是否高亮展示
        >
          {props.value}
        </button>
      );
    }
    
  6. 当无人获胜时,显示一个平局的消息:

        // 修改Game组件render中的结果判断
        let status = "";
        let full = true;
        cur.squares.forEach(item => {
          full = full && item;
        });
        if (winner) {
          status = `Winner is: ${winner}`;
        } else if (!winner && full) {
          status = `It's a draw!!!`;
        } else {
          status = `Next player: ${this.state.isX ? "X" : "O"}`;
        }
    

十、完成啦,代码如下:

index.js

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

function Square(props) {
  return (
    <button
      className="square"
      onClick={props.onClick}
      style={{color: props.isHigh ? "red": "black"}}
    >
      {props.value}
    </button>
  );
}

class Board extends React.Component {
  renderSquare(i, axis) {
    const highLight = this.props.highLight;
    const isHigh = highLight.includes(i);
    const randomKey = `cell${Math.floor(Math.random() * 900000 + 100000)}`;
    return <Square key={randomKey} value={this.props.squares[i]} onClick={() => this.props.onClick(i, axis)} isHigh={isHigh} />;
  }

  renderRow(j) {
    const rowACell = this.props.rowACell;
    let temp = [];
    for (let i = 0; i < rowACell; i++) {
      temp.push(this.renderSquare(i+rowACell*j, [j+1,i+1]));
    }
    const randomKey = `row${Math.floor(Math.random() * 900000 + 100000)}`;
    return(
      <div className="board-row" key={randomKey}>
        {temp}
      </div>
    )
  }

  renderBoard() {
    const rowACell = this.props.rowACell;
    let temp = [];
    for (let i = 0; i < rowACell; i++) {
      temp.push(this.renderRow(i));
    }
    return temp;
  }

  render() {
    return (
      <div>
        <div className="status">{this.props.status}</div>
        {this.renderBoard()}
      </div>
    );
  }
}

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
        axis: []
      }],
      isX: true,
      step: 0,
      rowACell: 3, // 3*3棋盘,如需可变,需要修改相应的calculateWinner()方法规则
      order: true, // 升序true,降序false
    }
  }

  handleClick(i, axis) {
    const history = this.state.history.slice(0, this.state.step + 1);
    const cur = history[history.length - 1];
    let temp = cur.squares.slice();
    const res = calculateWinner(temp);
    const winner = res.player;
    if (winner || temp[i]) {
      return;
    }
    temp[i] = this.state.isX ? "X" : "O";
    this.setState({
      history: history.concat([{
        squares: temp,
        axis: axis
      }]),
      isX: !this.state.isX,
      step: history.length
    });
  }

  jumpTo(i) {
    this.setState({
      step: i,
      isX: (i % 2) === 0
    });
  }

  changeOrder() {
    this.setState({
      order: !this.state.order
    })
  }

  historyCoder(moves) {
    if (this.state.order) {
      return moves;
    } else {
      return moves.reverse();
    }
  }

  render() {
    const history = this.state.history;
    const cur = history[this.state.step];
    const res = calculateWinner(cur.squares);
    const winner = res.player;

    const moves = history.map((item, i) => {
      const con = i ? `Go to step-${i}-[${item.axis[0]},${item.axis[1]}]` : `Go to Game start`;
      const randomKey = `moves${Math.floor(Math.random() * 900000 + 100000)}`;
      return(
        <li key={randomKey}>
          <button onClick={() => this.jumpTo(i)} className={this.state.step === i ? "active": ""}>{con}</button>
        </li>
      )
    });

    let status = "";
    let full = true;
    cur.squares.forEach(item => {
      full = full && item;
    });
    if (winner) {
      status = `Winner is: ${winner}`;
    } else if (!winner && full) {
      status = `It's a draw!!!`;
    } else {
      status = `Next player: ${this.state.isX ? "X" : "O"}`;
    }
    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={cur.squares}
            status={status}
            rowACell={this.state.rowACell}
            highLight={res.line}
            onClick={(i, axis) => this.handleClick(i,axis)}
          />
        </div>
        <div className="game-info">
          <div>
            <button onClick={() => this.changeOrder()}>{this.state.order ? "升序": "降序"}</button>
          </div>
          <ol>{this.historyCoder(moves)}</ol>
        </div>
      </div>
    );
  }
}

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

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

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 {
        player: squares[a],
        line: lines[i]
      };
    }
  }
  return {
    player: null,
    line: []
  };
}

index.css

body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

li {
  list-style: none;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  height: 60px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 60px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
  justify-content: center;
}

.game-info {
  margin-left: 20px;
}

.active {
  font-weight: bold;
  color: #38ccff;
}

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