React教程:以井字游戏简单游玩React

1 篇文章 0 订阅
1 篇文章 0 订阅

原链接:https://reactjs.org/tutorial/tutorial.html
个人学习使用,如有侵权请告知…

React简介

在我们开始之前
让我们做一个交互式井字游戏。

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

这里有一个成品,看起来很有意思的样子啊=v=,那我们来慢慢看这是怎么做出来的。

前提条件
如果你会HTML和js当然就很棒棒…不过官方表示我这种什么都不会的也可以往下看(官方你不要骗我= =).

如果你需要温习一下js,那么可以看看介个
需要注意的是我们使用了一些ES6的特性(js的最近的版本)。

In this tutorial, we’re using arrow functions, classes, let, and const statements. You can use the Babel REPL to check what ES6 code compiles to.

由于我并没有使用过,因此暂且不做详细了解。

如何跟着学一学呢
你可以在浏览器直接写代码,或者在本地配环境,怎么舒服怎么来(摸了)。

如果你想在浏览器上写
这是你想要开始的最快的方法!
直接在这里就好了。

如果你想在客户端开始
我先用浏览器体验一波,原链接有客户端的配置方法。

梗概
什么是React?
React是一种陈述的,高效的,灵活的用来建立用户接口的JS库。

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

我们会在短时间内获得有趣的类似XML的标签。你的组件告诉React你想渲染什么 - 然后React将有效地更新并在数据改变时渲染正确的组件。

现在,ShoppingList是一个React的组件类,或者称作组件类型。带参数的组件叫做props,返回通过render方法显示的视图层次结构。

render方法返回你想渲染的描述,随后React按这个描述把效果渲染到屏幕上。需要注意的是,render返回一个React element(React元素),它是一个轻量的,对你想要渲染的描述。大多数的React开发者用一种叫做JSX的特殊的语法,这能更简单的描述这种结构。div /语法在build期间,被转化成了React.createElement(‘div’).上述的例子可以被转化成如下形式:

return React.createElement('div', {className: 'shopping-list'},
  React.createElement('h1', /* ... h1 children ... */),
  React.createElement('ul', /* ... ul children ... */)
);

完整描述看这里
如果你对creatElement()很好奇的话,可以在这里看到它的API参考,但是我们不会在本教程中直接使用它,不过,我们会继续使用JSX。
您可以将任何JavaScript表达式放在JSX中的大括号内。 每个React元素都是一个真正的JavaScript对象,您可以将其存储在变量中或传递给您的程序。
ShoppingList只呈现内置的DOM组件,但是你可以通过编写轻松编写自定义的React组件。每个组件都被封装了起来,从而可以独立的操作。这样你就可以用简单的组件构造出复杂的UI了。

开始
先来看一个例子:还是我们的井字游戏
它包含了我们今天要建造的东西。我们提供了这些样式,因此你只需要担心JS的事情。
尤其是以下三个组件:

  1. Square
  2. Board
  3. Game
    Square组件渲染了一个,Board渲染了9个Square,Game组件渲染了一些我们可以填充在board里的占位物件。目前没有任何组件是交互的。

通过Prop传递数据
多说无益,让我们来试着从Board把数据传到Square吧。
在Board 的renderSquare方法中,让我们试着把一个值传给Square。

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

现在我们已经把Square的值赋上了,现在我们需要让他显示出来。

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

然后在Square里把值显示出来。

好了,我们可以开始使用交互组件了!
让Square组件通过点击,填入”X”!
尝试按如下改变render()的返回值:

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

如果你点击了square,你就会在浏览器中收到”click”的提醒。

这用到了JS的箭头函数语法。我们设立了一个onClick的参数,做onClick = {alert(’click’)}会立即发出警报,而不是点击按钮的时间。

React组件可以通过设置this.state在constructor中,从而建立状态,它应当被认为是组件私有的。让我们把当前的square的值存储在状态中,通过点击来改变它。
首先,加入一个constructor来初始化状态。

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

  render() {
    return (
      <button className="square" onClick={() => alert('click')}>
        {this.props.value}
      </button>
    );
  }
}

在JS中,如果需要构造子类的constructor,那么需要明确地调用super()

现在改变Square的render方法通过触发点击事件来改变当前的状态。
1. 将< button >中的this.props.value改变成this.state.value
2. 将()=>alert()改变成this.setState({value:’X’})
更改代码如下:

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

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

开发者工具
这个看一看原文章吧=v=。

开始改变状态!
我们已经有了这个游戏的基础部分,但是现在,每个Square的状态还被封装在各自的组件内部。为了做成一个完整的游戏,我们现在需要检查玩家是否完成了这个游戏,以及轮流放置’X’和’O’。我们需要同时知道所有9个方块的状态,而不是让他们分别在各自的组建中。

你可能认为我们需要访问Board中的每一个Square,尽管这是可行的,但是这会使得代码更加的难理解,脆弱和难以重构。

最好的解决方案是把每个Square的状态存储在Board中,并且由Board告诉Square它该显示什么,这样就可以达到之前所表现出来的效果。

如果你想要汇总多个子组件的数据,或者让两个子组件彼此交流,那么把状态上调至父组件中*。父组件可以把状态通过props下发到子组件中,从而使得子组件彼此都是和父组件同步的。

将状态拉到上一级,是在重构React组件时常做的一件事情,现在就可以试一试。给Board添加一个constructor,将其设置为一个大小为9个空内容的数组,对应9个Square。

constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

然后将Board的状态通过renderSquare传给Square。

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

当前代码如下:

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 {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }
  renderSquare(i) {
    return <Square value = {this.state.squares[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')
);

第一个事情已经完成了,我们接下来在点击square之后改变其状态。现在Board组件存储了哪些squares被填充了,这意味着我们需要某些方式去更新Board的状态。鉴于组件的状态都是私有的,所以我们无法直接通过Square去更新Board的状态。

常用的模式是从父到子传下去一个函数监听子组件是否被点击。我们把renderSquare稍加修改。

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

为了增加可读性,我们将return的元素变成了多行,并且增加了分号,以便JavaScript在返回后不插入分号并中断代码。

现在我们从Board传下去了两个props到Squares:valueonClick。后者是一个Squares可以调用的函数。接下来对Square做出如下改变:
1. 将Square内的renderthis.state.value改变成this.props.value
2. 将this.setState()改成this.props.onClick()
3. 删除Square中的constructor,因为它已经没有状态了。
更改之后的Squares组件如下:

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

当square被点击之后,它调用了Board传下来的函数onClick,来看看这个过程是怎么样的:
1. 内置在DOM< button >组件中onClick 这个prop告诉React去建立一个点击事件监听器
2. 当按钮被按下,React会执行Square的render()函数,然后执行onClick事件
3. 事件处理程序会激活this.props.onClick(),Square的props是由Board规定的
4. Board通过onClick={() => this.handleClick(i)},从而运行Board上的this.handleClick(i)
5. 我们还没有定义this.handleClick(i),因此程序崩溃了

那我们来给他加上>3<。

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares:squares});
  }

.slice()的目的是复制squares,而不是直接更改它。

现在你应该可以点击方格来填充它们,但是状态被存储在Board组件中而不是在每个Square中,这让我们继续构建游戏。 注意主板状态发生变化时,Square组件会自动重新渲染。
Square不再有自己的状态了,它从父组件Board获得value,并且在被点击的时候通知它的父组件,这种组件被称为受控组件

为什么不变性(不直接更改数组)是很重要的呢?
在之前的代码举例中,我们建议使用.slice()来复制数组以防止更改已有数组。现在来看一下为什么这是一个需要学习的重要观念。

有两种改变数据的方式
1. 直接修改数据
2. 复制一份原有数据之后,再修改新生成的数据。

最终结果是相同的,但通过不直接改变(或更改基础数据),我们现在可以获得额外的好处,可以帮助我们提高组件和整体应用程序性能。

更简单的撤销和重做,以及时间回滚

对比变化
等等等…我先了解到这里,看起来并没有什么太大的意义。

功能组件
我们已经移除了constructor,事实上React支持一种叫做functional components(功能组件)的组件,专门为了Square这种只包含render的类,而不用定义一个继承React.Component
就是简单写一个function包含props,并且返回需要被rendered的东西。
用这个函数来替代整个Square class:

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

轮流下子
现在我们的游戏中只有’X’能动。
我们规定游戏从’X’开始移动,将下一步的棋子类型加入Board的状态中。

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

然后每次click的时候进行flip:

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

随后修改消息提醒:

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

完整的代码如下:

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

  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 <Square 
             value = {this.state.squares[i]}
             onClick = {()=>this.handleClick(i)}
             />;
  }

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

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

你可以在Board的render函数中check当前的状态是否有人完成游戏或者下一步是谁走。

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

至此,这个游戏已经可以玩了!

代码如下:

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

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

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

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

  render() {
    const winner = CalculateWinner(this.state.squares);
    let status;
    if(winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next Player :' + (this.state.xIsNext?'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')
);

存储历史局面
(可持久化一下)
我们要建立一个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,使得它控制着square,并且它自己的onClick由Game控制,如同之前更改square一样。

一点都不好玩,咕咕了>A<

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值