状态提升

至此,我们已经拥有了编写井字棋游戏的基本构件。但现在,状态(state)是被包裹在各个Square组件内的。为了完成这个游戏,我们还需要做这两件事:检查是否已经有玩家胜出;以及在小方格中轮流填入“X”和“O”。为了检查是否已经有玩家获胜,我们需要把9个小方格的状态值都集中到一个地方,而不是让它们分散在各个Square组件内部。

你可能会想到,让Board组件去查询各个Square组件的当前状态值。当然,单纯从技术上讲,用React是能做到这个的,但我们并不鼓励这么干。因为这会让代码变得不易理解,更脆弱,也更难重构。

所以,最佳的方案,是把状态值都存储到Board组件,而非各个Square组件中。这样,Board组件就可以告诉各个Square组件应该显示什么。这就跟之前,我们让每个小方格显示各自序号所用的方法是一样的。

当你需要从多个子组件中聚集数据,或者想让两个子组件互相通信的时候,你应该把状态提升到父组件之中。父组件可以通过props把状态值传回其子组件。如此,子组件互相之间、子组件和父组件之间都能保持同步。

在重构React组件时,像这样提升状态的做法是非常常见的。借着这次机会,我们也来试一下。在Board组件中,添加constructor函数,并设置初始状态:一个包含9个null的数组,它们分别对应9个小方格。

code

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

  renderSquare(i) {
    return <Square value={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>
    );
  }
}

待会儿,我们会填入一些东西,让它变成类似这样:

code

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

现在,Board组件的renderSquare方法是这样子的:

code

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

修改它,把value属性传给Square组件:

code

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

查看最新的代码

现在,我们来改变小方块被点击后的行为。Board组件存储着填小方块的东西,这意味着我们需要想办法让Square组件更新Board的状态。因为状态是组件私有的,所以我们不能直接从Square组件修改Board组件的状态。

通常的方法是这样的:从Board组件向Square组件传一个函数,让它在小方块被点击时执行。再次修改Board组件中的renderSquare方法,让它变成这样:

code

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

为了提高可读性, 我们把这个被返回的元素分开写成多行。再用括号括住它.这样能防止JavaScript在return后面加个分号而打断代码语句。

现在,我们从Board组件向Square组件传递了两个属性:valueonClick,后者是Square组件可以呼叫的函数。我们继续对Square组件做如下改动:

  • 把Square组件的render函数中的this.state.value替换为this.props.value;
  • 把Square组件的render函数中的this.setState()替换为this.props.onClick();
  • 从Square组件中删去constructor函数,因为它已经不包含任何状态了。

做了以上改动后,这个组件成了这样子:

code

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

现在,当小方格被点击时,会呼叫从Board组件传来的onClick函数。主要过程如下:

  1. built-in DOM <button> component中的onClick属性通知React设置一个点击事件监听器;
  2. 当按钮被点击,React将会呼叫在Square组件中render()方法里定义的onClick事件处理器;
  3. 该事件处理器呼叫this.props.onClick()。Square组件的props由Board组件规定;
  4. Board组件将onClick={() => this.handleClick(i)}传给了Square组件,所以,当被呼叫时,Board组件中运行this.handleClick(i)
  5. 我们目前还没有在Board组件中定义handleClick()方法,所以代码会出错。

???需要注意的是, DOM <button>组件中的onClick对React有着特别的意义。???我们本可以把Square组件中的onClick和Board组件中的handleClick叫成别的名字。然而,React app中有约定俗成的方式:对于处理器属性用on*的格式命名;对于其implementation,用handle*的格式命名。 (fixme)

请试着点击小方格。应该会收到报错信息,因为我们还没有定义handleClick。现在,把它加到Board组件的类中。

code

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

查看最新的代码

我们用.slice()来拷贝一份squares数组的副本,再对副本进行操作。不要直接改变原数组。查看这一部分来了解不可改变性( immutability)的重要性。

如果现在再点击小方格,格子里应该又会出现“X”了。但此时,状态值是存储在Board组件里的,而不是像之前,存在各个Square组件中。这让我们的游戏编写工作得以继续进行。注意,无论Board组件中的状态值何时改变,Square组件总能自动重新渲染。

Square组件不再保有自己的状态,而是改为从父组件,即Board组件那里接收;同时,当它被点击的时候,会通知其父组件。我们把这种组件叫做受控组件

results matching ""

    No results matching ""