React 实现井字棋游戏 (tic-tac-toe) 教程 (3) <译自官方文档>
2017-11-05 09:37
651 查看
React 实现井字棋游戏 (tic-tac-toe) 教程 (1) <译自官方文档>
React 实现井字棋游戏 (tic-tac-toe) 教程 (2) <译自官方文档>
4000
你可能会想到,让 Board 组件去查询各个 Square 组件的当前状态值。当然,单纯从技术上讲,用 React 是能做到这个的,但我们并不鼓励这么干。因为这会让代码变得不易理解,更脆弱,也更难重构。
所以,最佳的方案,是把状态值都存储到 Board 组件,而非各个 Square 组件中。这样,Board 组件就可以告诉各个 Square 组件应该显示什么。这就跟之前,我们让每个小方格显示各自序号所用的方法是一样的。
当你需要从多个子组件中聚集数据,或者想让两个子组件互相通信的时候,你应该把状态提升到父组件之中。父组件可以通过props把状态值传回其子组件。如此,子组件互相之间、子组件和父组件之间都能保持同步。
在重构 React 组件时,像这样提升状态的做法是非常常见的。借着这次机会,我们也来试一下。在 Board 组件中,添加 constructor 函数,并设置初始状态:一个包含9个 null 的数组,它们分别对应9个小方格。
code
待会儿,我们会填入一些东西,让它变成类似这样:
code
现在,Board 组件的
code
修改它,把
code
查看最新的代码
现在,我们来改变小方块被点击后的行为。Board 组件存储着填小方块的东西,这意味着我们需要想办法让 Square 组件更新 Board 的状态。因为状态是组件私有的,所以我们不能直接从 Square 组件修改 Board 组件的状态。
通常的方法是这样的:从 Board 组件向 Square 组件传一个函数,让它在小方块被点击时执行。再次修改 Board 组件中的
code
为了提高可读性, 我们把这个被返回的元素分开写成多行。再用括号括住它.这样能防止 JavaScript 在 return 后面加个分号而打断代码语句。
现在,我们从 Board 组件向 Square 组件传递了两个属性:
把 Square 组件的
把 Square 组件的
从 Square 组件中删去
做了以上改动后,这个组件成了这样子:
code
现在,当小方格被点击时,会呼叫从 Board 组件传来的
built-in DOM
当按钮被点击,React 将会呼叫在 Square 组件中
该事件处理器呼叫
Board 组件将
我们目前还没有在 Board 组件中定义
需要注意的是, DOM
请试着点击小方格。应该会收到报错信息,因为我们还没有定义
code
查看最新的代码
我们用
如果现在再点击小方格,格子里应该又会出现“X”了。但此时,状态值是存储在 Board 组件里的,而不是像之前,存在各个 Square 组件中。这让我们的游戏编写工作得以继续进行。注意,无论 Board 组件中的状态值何时改变,Square 组件总能自动重新渲染。
Square 组件不再保有自己的状态,而是改为从父组件,即 Board 组件那里接收;同时,当它被点击的时候,会通知其父组件。我们把这种组件叫做受控组件。
通常来说,修改数据的方法有两种。第一种方法是通过直接改动变量的值来修改(mutate)原有数据,第二种方法是使用一份改动后的副本,以此替换(replace)原有数据。
改动(mutate)原数据
code
不改动(mutate)原数据
code
最终的结果是一样的。但是,不直接改动基础数据的方法却能带来一些额外的好处:它有助于提升组件或者整个应用的性能。
而判断不可变对象是否被改动则是相当容易的。 如果被引用的对象与之前的不同,则对象已更改。就这么简单。
想要了解
用下面这个函数替换掉整个 Square 的类:
code
你需要把两个
整理代码时,我们把
查看最新的代码
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) <译自官方文档>相关文章推荐
- React 实现井字棋游戏 (tic-tac-toe) 教程 (5) <译自官方文档>
- React 实现井字棋游戏 (tic-tac-toe) 教程 (6) <译自官方文档>
- React 实现井字棋游戏 (tic-tac-toe) 教程 (4) <译自官方文档>
- React 实现井字棋游戏 (tic-tac-toe) 教程 (1) <译自官方文档>
- React 实现井字棋游戏 (tic-tac-toe) 教程 (2) <译自官方文档>
- << CocoaPods安装和使用教程 >>github code4app以及cocoachina 苹果官方文档
- Android 官方文档:(二)应用清单 —— 2.26 <uses-permission>标签
- 纯C++游戏编程: Tic-Tac-Toe(三连棋游戏)的实现
- Android实战简易教程<五十一>(ListView实现子控件的动态显示和隐藏、checkbox全选和反选)
- Android实战简易教程<三十八>(模仿腾讯QQ的网络状态提示和设置功能实现)
- Android实战简易教程<二十五>(基于Baas的数据表查询下拉刷新和上拉加载实现!)
- 用TypeScipt和AMD模块化理念实现React官方教程(四)获取数据
- [LeetCode] Design Tic-Tac-Toe 设计井字棋游戏
- Android 官方文档:(二)应用清单 —— 2.2 <action>标签
- C++alpha beta剪枝算法 实现4*4 tic-tac-toe
- Android实战简易教程<十四>(介绍SwipeMenuListView-简单实现类似QQ的滑动删除效果)
- 使用Python编写一个简单的tic-tac-toe游戏的教程
- [CareerCup] 17.2 Tic Tac Toe 井字棋游戏
- 用TypeScipt和AMD模块化理念实现React官方教程(五)提交和更新数据
- Managing Your App's Memory —— Android官方文档翻译<一>