React官网井字棋教程详细分析

井字棋小游戏

官网有详细的初始环境配置过程,可以对比这篇教材详解进行学习:官网入门教程跳转链接

一个 package 管理器,比如 Yarn 或 npm。
它能让你充分利用庞大的第三方 package 的生态系统,并且轻松地安装或更新它们。
一个打包器,比如 webpack 或 Parcel。
它能让你编写模块化代码,并将它们组合在一起成为小的 package,以优化加载时间。
一个编译器,例如 Babel。
它能让你编写的新版本 JavaScript 代码,在旧版浏览器中依然能够工作。

初级

1、开启环境

npx create-react-app my-app
cd my-app
npm start

删除src里面的内容,保留文件夹

cd my-app    #找到文件位置
cd src          # 如果你使用 Windows:
del *             #删除掉
cd ..                #返回项目文件

在 src/ 文件夹中创建一个名为 index.css 的文件,并拷贝这些CSS 代码

在 src/ 文件夹下创建一个名为 index.js 的文件,并拷贝这些 JS 代码

拷贝以下三行代码到 src/ 文件夹下的 index.js 文件的顶部:

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

现在,在项目文件夹下执行 npm start 命令,然后在浏览器访问 http://localhost:3000 。
这样你就可以在浏览器中看见一个空的井字棋的棋盘了。
注意,这里只要更改了js或者css,代码会自动重新运行渲染。完成后的vscode界面
这是启动后的截图
2、解释代码运行原理
状态提升的思想
每个 Square 组件都维护了游戏的状态。我们可以把所有 9 个 Square 的值收集起来,用来判断游戏的优胜者。

如果在棋盘 Board 组件中一个个收集每个格子 Square 组件中的 state。虽然技术上来讲可以实现,但是代码如此编写会让人很难理解,并且我们以后想要维护重构时也会非常困难。因为state分布在各个square里面,就会嵌套得很复杂。

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

class Board extends React.Component {				//这个是父组件
  renderSquare(i) {    return <Square />;  }		//就是为了能够把位置信息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>
    );
  }
}

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

最好的解决方式是直接将所有的 state 状态数据存储在 Board 父组件当中。之后 Board 组件可以将这些数据通过 props (不可变)传递分发给各个 Square 子组件,正如上文我们把数字传递给每一个 Square 一样。

当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,需要把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中所有组件的状态数据就能够更方便地同步共享了。
专门解释props和state的文章:下面是完好的的代码JS部分

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>  );
}			//返回一个按钮,如果点击下去

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();						//slice出新数组
    if (calculateWinner(squares) || squares[i]) {   return;  }	   //判断胜负,已经赢了直接跳过。没有再改变点击的属性
    squares[i] = this.state.xIsNext ? 'X' : 'O';		//直接改变点击后需要反转的值,是X还是O
    this.setState({					//然后把新的数组更新到旧数组去,反转判断的布尔值也改变
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }
  
//这里一个是被动改变的值,收集9个以后用来判断输赢。另一个是主动的鼠标点击事件,改变state的OX和前面那个输赢判断值
  renderSquare(i) {			//返回一个棋子,然后传入值(来自父组件的向下分发),传入点击事件处理函数,
    return (					//如果不传这些,子组件无法使用父组件函数,所以只能在这里通过props来向下传递分发
      <Square        value={this.state.squares[i]}        onClick={() => this.handleClick(i)}      />
    );
  }

  render() {
    const winner = calculateWinner(this.state.squares);		//先传入输赢判断的依据去判断输赢
    let status;											//定义出提示文本,下面return出提示文本的内容	
    if (winner) {      status = 'Winner: ' + winner;    } 
    	else {      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'   );
   }

    return (											//这里是基本的井字棋布局,提示信息+9宫格
      <div>
        <div className="status">{status}</div>
        <div className="board-row">			//下面传state也可以,直接传函数,拿子组件去渲染也可以,不过传子组件配参数,应该ok
          {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'));

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];								//里面的值是X或者O,所以可以直接返回出来,就知道谁是赢家了
    }
  }
  return null;
}

到此为止,已经完成了基本功能
1、一开始从结构入手,不考虑数据流向,,选择使用父组件渲染九宫格期盼,子组件定义棋子的所有信息,外表交给css来完成。
2、后来考虑数据流向,在子组件上收集state信息进行胜负判断嵌套严重,所以把state的状态提升到父组件,通过父组件下发state给子组件。在父组件中进行胜负判断,而子组件只要负责点击事件和显示(在按钮处即可实现)即可,在不需要state的时候,就可以用函数形式来写子组件。

进阶

加入悔棋功能,甚至可以跳转到任意时间段

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

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)        }      ],
      stepNumber: 0,
      xIsNext: true
    };
  }

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

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

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

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

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

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

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 squares[a];
    }
  }
  return null;
}


留疑解答(非唯一)

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值