react官方文档棋盘案例分析

6 篇文章 0 订阅

功能介绍

井字棋

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

初始状态代码

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

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

  • Square
  • Board
  • Game
    Square 组件渲染了一个单独的 <button>。Board 组件渲染了 9 个方块。Game 组件渲染了含有默认值的一个棋盘,我们一会儿会修改这些值。到目前为止还没有可以交互的组件。

通过 Props 传递数据

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

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

修改 Square 组件中的 render 方法,把 {/* TODO */} 替换为 {this.props.value},以显示上文中传入的值:

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

给组件添加交互功能

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

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

接下来,我们希望 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 (
      <button className="square" onClick={() => alert('click')}>
        {this.props.value}
      </button>
    );
  }
}

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

<button>标签中,把 this.props.value 替换为this.state.value
onClick={...}事件监听函数替换为 onClick={() => this.setState({value: 'X'})}
为了更好的可读性,将 classNameonClickprop 分两行书写。
修改之后,Square 组件中 render 方法的返回值中的 <button> 标签就变成了下面这样:

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

在 Square 组件 render 方法中的 onClick 事件监听函数中调用 this.setState,我们就可以在每次 被点击的时候通知 React 去重新渲染 Square 组件。组件更新之后,Square 组件的 this.state.value 的值会变为 ‘X’,因此,我们在游戏棋盘上就能看见 X 了。点击任意一个方格,X 就会出现了。

每次在组件中调用 setState 时,React 都会自动更新其子组件。

状态提升

当前,每个 Square 组件都维护了游戏的状态。我们可以把所有 9 个 Square 的值放在一个地方,这样我们就可以判断出胜者了。

你可能会想,我们也可以在棋盘 Board 组件中收集每个格子 Square 组件中的 state。虽然技术上来讲是可以实现的,但是代码如此编写会让人很难理解,并且我们以后想要维护重构时也会非常困难。所以,最好的解决方式是直接将所有的 state 状态数据存储在 Board 父组件当中。之后 Board 组件可以将这些数据通过 props 传递给各个 Square 子组件,正如上文我们把数字传递给每一个 Square 一样。

当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,需要把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中所有组件的状态数据就能够更方便地同步共享了。

像这种将组件的 state 提升到父组件的情形在重构 React 组件时经常会遇到 —— 借此我们来实践一下。

为 Board 组件添加构造函数,将 Board 组件的初始状态设置为长度为 9 的空值数组:

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

  renderSquare(i) {
    return <Square value={i} />;
  }

Board 组件当前的 renderSquare 方法看起来像下面这样:

 renderSquare(i) {
    return <Square value={i} />;
  }

开始时,我们依次使把 0 到 8 的值通过 prop 从 Board 向下传递,从而让它们显示出来。上一步与此不同,我们根据 Square 自己内部的 state,使用了 “X” 来代替之前的数字。因此,Square 忽略了当前从 Board 传递给它的那个 value prop。

让我们再一次使用 prop 的传递机制。我们通过修改 Board 来指示每一个 Square 的当前值(‘X’, ‘O’, 或者 null)。我们在 Board 的构造函数中已经定义好了 squares 数组,这样,我们就可以通过修改 Board 的 renderSquare 方法来读取这些值了。

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

这样,每个 Square 就都能接收到一个 value prop 了,这个 prop 的值可以是 ‘X’、 ‘O’、 或 null(null 代表空方格)。

接下来,我们要修改一下 Square 的点击事件监听函数。Board 组件当前维护了那些已经被填充了的方格。我们需要想办法让 Square 去更新 Board 的 state。由于 state 对于每个组件来说是私有的,因此我们不能直接通过 Square 来更新 Board 的 state。

相反,从 Board 组件向 Square 组件传递一个函数,当 Square 被点击的时候,这个函数就会被调用。接着,我们将 Board 组件的 renderSquare 方法改写为如下效果:

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

现在我们从 Board 组件向 Square 组件中传递两个 props 参数:value 和 onClick。onClick prop 是一个 Square 组件点击事件监听函数。接下来,我们需要修改 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>
    );
  }
}

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

  1. 向 DOM 内置元素 <button> 添加 onClick prop,让 React 开启对点击事件的监听。
  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”。
    我们现在来向 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 (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(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>
    );
  }
}

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

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

注意,我们调用了 .slice() 方法创建了 squares 数组的一个副本,而不是直接在现有的数组上进行修改。

函数组件

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

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

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

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

轮流落子

现在井字棋还有一个明显的缺陷有待完善:目前还不能在棋盘上标记 “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 (
      <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>
    );
  }
}

判断出胜者

至此我们就可以看出下一步会轮到哪位玩家,与此同时,我们还需要显示游戏的结果来判定游戏结束。拷贝如下 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,
    });
  }

时间旅行

实现“回到过去”的功能,从而在游戏里跳回到历史步骤。

保存历史记录

如果我们直接修改了 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',
    ]
  },
  // ...
]

再次提升状态

我们希望顶层 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 (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

下一步,我们让 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 (
      <Square
        value={this.props.squares[i]}
        onClick={() => 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 (
      <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>
    );
  }
}

接着,更新 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 方法中对应的代码移除。修改之后,Board 组件的 render 函数如下所示:

 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 方法,因为这两个组件的 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,
    });
  }

到目前为止,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 (
        <li>
          <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>
    );
  }

选择一个 key

因为 React 无法得知我们人类的意图,所以我们需要给每一个列表项一个确定的 key 属性,它可以用来区分不同的列表项和他们的同级兄弟列表项。你可以使用字符串,比如 alexa, ben, claudia。如果我们使用从数据库里获取的数据,那么 Alexa、Ben 和 Claudia 的数据库 ID 就可以作为 key 来使用。

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

每当一个列表重新渲染时,React 会根据每一项列表元素的 key 来检索上一次渲染时与每个 key 所匹配的列表项。如果 React 发现当前的列表有一个之前不存在的 key,那么就会创建出一个新的组件。如果 React 发现和之前对比少了一个 key,那么就会销毁之前对应的组件。如果一个组件的 key 发生了变化,这个组件会被销毁,然后使用新的 state 重新创建一份。

key 是 React 中一个特殊的保留属性(还有一个是 ref,拥有更高级的特性)。当 React 元素被创建出来的时候,React 会提取出 key 属性,然后把 key 直接存储在返回的元素上。虽然 key 看起来好像是 props 中的一个,但是你不能通过 this.props.key 来获取 key。React 会通过 key 来自动判断哪些组件需要更新。组件是不能访问到它的 key 的。

我们强烈推荐,每次只要你构建动态列表的时候,都要指定一个合适的 key。如果你没有找到一个合适的 key,那么你就需要考虑重新整理你的数据结构了,这样才能有合适的 key。

如果你没有指定任何 key,React 会发出警告,并且会把数组的索引当作默认的 key。但是如果想要对列表进行重新排序、新增、删除操作时,把数组索引作为 key 是有问题的。显式地使用 key={i} 来指定 key 确实会消除警告,但是仍然和数组索引存在同样的问题,所以大多数情况下最好不要这么做。

组件的 key 值并不需要在全局都保证唯一,只需要在当前的同一级元素之前保证唯一即可。

实现时间旅行

在井字棋的历史记录中,每一个历史步骤都有一个与之对应的唯一 ID:这个 ID 就是每一步棋的序号。因为历史步骤不需要重新排序、新增、删除,所以使用步骤的索引作为 key 是安全的。

在 Game 组件的 render 方法中,我们可以这样添加 key,

  • ,这样关于 key 的警告就会消失了。
  •  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>
          );
        });
    

    在我们实现 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);
    
        // 其他部分没有改变
    

    最终代码实现

    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;
    }
    
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值