您的位置:首页 > Web前端 > React

React 实现井字棋游戏 (tic-tac-toe) 教程 (3) <译自官方文档>

2017-11-05 09:37 651 查看
React 实现井字棋游戏 (tic-tac-toe) 教程 (1) <译自官方文档>

React 实现井字棋游戏 (tic-tac-toe) 教程 (2) <译自官方文档>

3-状态提升

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

4000
你可能会想到,让 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 组件传递了两个属性:
value
onClick
,后者是 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
函数。主要过程如下:

built-in DOM
<button>
component 中的
onClick
属性通知React设置一个点击事件监听器;

当按钮被点击,React 将会呼叫在 Square 组件中
render()
方法里定义的
onClick
事件处理器;

该事件处理器呼叫
this.props.onClick()
。Square 组件的 props 由 Board 组件规定;

Board 组件将
onClick={() => this.handleClick(i)}
传给了 Square 组件,所以,当被呼叫时,Board 组件中运行
this.handleClick(i)


我们目前还没有在 Board 组件中定义
handleClick()
方法,所以代码会出错。

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

请试着点击小方格。应该会收到报错信息,因为我们还没有定义
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 组件那里接收;同时,当它被点击的时候,会通知其父组件。我们把这种组件叫做受控组件

为什么不可变性很重要

在之前的示例代码中,我们建议使用
.slice()
运算符来拷贝一份
squares
数组,再在其副本上进行数据改动,以防止原有的数组被修改。现在,我们来具体谈谈它的内涵,和这么做的重要性。

通常来说,修改数据的方法有两种。第一种方法是通过直接改动变量的值来修改(mutate)原有数据,第二种方法是使用一份改动后的副本,以此替换(replace)原有数据。

改动(mutate)原数据

code

var player = {score: 1, name: 'Jeff'};
player.score = 2;
// 现在 player 是 {score: 2, name: 'Jeff'}


不改动(mutate)原数据

code

var player = {score: 1, name: 'Jeff'};

var newPlayer = Object.assign({}, player, {score: 2});
// 现在 player 没有改变, 而 newPlayer 是 {score: 2, name: 'Jeff'}

// 或者使用对象展开符:
// var newPlayer = {...player, score: 2};


最终的结果是一样的。但是,不直接改动基础数据的方法却能带来一些额外的好处:它有助于提升组件或者整个应用的性能。

更简单的 撤销/重做 和 穿越功能

不可改变性 也能让一些复杂的特性实现起来更容易。例如,在本教程后期,我们将要实现在棋局的不同阶段间穿越的功能。避免数据的变动(mutation),能让我们保持对旧版本数据的引用。如果我们需要的话,就能在它们之间切换。

追踪变动

对于被直接改动(mutate)的对象,我们难以判断它们是否被修改,因为所以改动都直接在原对象上进行的。这要求比较当前对象和之前的拷贝的副本,遍历整个对象数,比较每个变量与值。这个过程可能会变得越来越复杂。

而判断不可变对象是否被改动则是相当容易的。 如果被引用的对象与之前的不同,则对象已更改。就这么简单。

在React中 确定何时重新渲染

在React中,当建立纯组件时,不可改变性带来的好处最明显。对于不可改变的数据,我们能很容易地确认改动是否发生,借此,我们就可以确定组件何时要求被重新渲染。

想要了解
shouldComponentUpdate()
以及如何构建纯组件,请查看优化性能

函数式声明组件

我们已经移除了 Square 组件的 constructo r函数。事实上,对于 Square 这样,仅仅由
render
方法构成的组件,React 有一种更简单的声明组件的语法,叫函数式声明组件。不必用
extends React.Component
来定义组件,你仅仅只需写一个函数,它接受属性,返回需要被渲染的东西即可。

用下面这个函数替换掉整个 Square 的类:

code

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


你需要把两个
this.props
都换成
props
。你的 app 中,很多组件都能写成函数式声明的组件。这样的组件更容易写,而且 React 以后也会继续优化它们。

整理代码时,我们把
onClick={() => props.onClick()}
也换成
onClick={props.onClick}
。因为对于本案例来说,把函数传下来就已经足够了。注意,写成
onClick={props.onClick()}
是不行的,因为它会立即调用
props.onClick
,而不是如我们所想的把它传下来。

查看最新的代码

更新

React 实现井字棋游戏 (tic-tac-toe) 教程 (4) <译自官方文档>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐