React的井字过三关(1)
React的井字过三关(1)
本文系React官方教程的Tutorial: Intro To React的笔记。由笔者用ES5语法改写。
在本篇笔记中,尝试用React构建一个可交互的井字棋游戏。
开始
先布局:
status反映游戏信息。九宫格采用flex布局。右侧有一处游戏信息。
<div id="container">
<div class="game">
<div class="board">
<div class="status">Next player: X</div>
<div class="board-row">
<button class="square"></button>
<button class="square"></button>
<button class="square"></button>
</div>
<div class="board-row">
<button class="square"></button>
<button class="square"></button>
<button class="square"></button>
</div>
<div class="board-row">
<button class="square"></button>
<button class="square"></button>
<button class="square"></button>
</div>
</div>
<div class="info">
<div></div>
<ol></ol>
</div>
</div>
</div>
再把css写一下:
/*Simple CSS-Reset*/
*{
margin:0;
padding:0;
}
body{
font: 30px "Century Gothic", Futura, sans-serif;
margin: 20px;
}
ul{
list-style: none;
}
a{
text-decoration: none;
}
ol, ul{
padding-left: 30px;
}
/*major*/
#container{
width: 500px;
margin:0 auto;
}
.game{
display: flex;
flex-direction: row;
}
.status{
margin-bottom: 20px;
text-align: center;
}
.board-row:after{
clear: both;
content: "";
display: table;
}
.square{
background: #fff;
border: 1px solid #999;
float: left;
font-size: 36px;
font-weight: bold;
line-height: 100px;
height: 100px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 100px;
}
#container .square:focus {
background: #ddd;
outline: none;
}
.info {
margin-left: 30px;
font-size:20px;
}
基本效果:
接下来只需要考虑javascript实现就可以了。
整个应用分为三个组件:
- Square(方块)
- Board(九宫格面板)
- Game(整个游戏)
接下来就是把这个结构用React写出来。
var Game=React.createClass({
render:function(){
return (
<div className="game">
<Board />
<div className="info">
<div></div>
<ol></ol>
</div>
</div>
);
}
});
var Board=React.createClass({
renderSquare:function(i){
return <Square />
},
render:function(){
return (
<div clasName="board">
<div className="status">Next player: X</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>
);
}
});
var Square=React.createClass({
render:function(){
return (
<button className="square"></button>
);
}
});
ReactDOM.render(
<Game />,
document.getElementById('container')
);
通过props传递数据
现在尝试从Board组件中传递一些数据给Square组件:
var Board=React.createClass({
renderSquare:function(i){
return <Square value={i} />
},
...
Square内部:
var Square=React.createClass({
render:function(){
return (
<button className="square">{this.props.value}</button>
);
}
});
数字就被打上去了。
交互的组件
当点击方块时,打出“X”。
先把Square设置初始的state.value为null。当点击小方框,触发一个changeState
方法。把当下的State改为X
.
然后把渲染方法改为:
var Square=React.createClass({
getInitialState:function(){
return {
value:null
}
},
changeState:function(){
this.setState({
value:'X'
})
},
render:function(){
return (
<button className="square" onClick={this.changeState}>{this.state.value}</button>
);
}
});
基本效果:
无论何时,this.setState
只要被调用,组件将马上更新并根据状态渲染它的后代。
通过开发者工具看组件树
插播一个广告:React为开发者提供了适用于火狐及Chrome的扩展工具。有了它可以很方便看到你构建的组件库。
当然Google商店现在得FQ才行。在安装之后,勾选“允许访问本地网址”,便可激活。
解除状态
现在,井字棋已经有了个雏形。但是State被锁定在每个单独小的方块中。
为了让游戏能够正常进行,还需要做一些事情:
- 判断胜负
X
和O
的交替
为了判断胜负,我们需要将9个方块的value放到一块。
你可能会想,为什么Board
组件为什么不查询每个组件的状态并进行计算?——这在理论上是可行的。但是React不鼓励这样做——这样导致代码难读,脆弱,变得难以重构。
相反,最好的解决方案就是把state放到Board
组件上,而不是每个方块里。Board
组件可以告诉每个小方块要显示什么——通过之前加索引值的方法。
当你先从各种各样的子代中把它们的数据统一起来,那就把state放到它们的父级组件上吧!然后通过props
把数据全部传下去。子组件就会根据这些props同步地展示内容。
在React里,组件做不下去的时候,把state
向上放是很常见的处理办法。正好借此机会来试一下:设置Board
组件的状态——为一个9个元素的数组(全部是null),以此对应九个方块:
var Board=React.createClass({
getInitialState:function(){
return (
squares:Array(9).fill(null),
)
},
...
到了后期,这个状态可以指代一个棋局,比如这样:
[
'O', null, 'X',
'X', 'X', 'O',
'O', null, null,
]
然后把这个状态数组分配到每个小方块中(还记得renderSquare方法吗?):
renderSquare:function(i){
return <Square value={this.state.squares[i]} />
},
再次把Square
的组件改为{this.props.value}
。现在需要改变点击事件的方法。当点击小方块,通过回调props传入到Square
中,直接把Board
组件state相应的值给改了:
return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />
这里的onClick
不是一个事件。而是方块组件的一个props。现在方块组件Square
接受到这个props方法,就把它绑定到真正的onClick上面:
<button className="square" onClick={() => this.props.onClick()}>{this.props.value}</button>
补白:ES6的箭头函数
x => x * x
以上的意思是:
function (x) {
return x * x;
}
箭头函数相当于匿名函数,并且简化了函数定义。
React在此引入箭头函数处理的是this的问题。
如果不用箭头函数写是:
renderSquare:function(i){
var _this=this;
return <Square onClick={function(){return _this.handleClick(i)}} value={this.state.squares[i]} />
},
选择自己喜欢的就好。
现在根据就差定义面板组件中handleClick
函数了。显然点击一下就刷新Board
的状态。以下两种方法都可以。
handleClick:function(i){
this.setState(function(prev){
//console.log(prev.squares)
var arr=prev.squares;
arr.squares[i]='X';
return {
squares:prev.arr
};
})
},
handleClick:function(i){
var squares=this.state.squares.slice();
squares[i]='X';
this.setState({
squares:squares
})
},
把状态往上放,使得每个小方框不再拥有自己的状态。面板组件会分配props给他们。只要状态改变,下面的组件就会更新。
为什么不突变的数据很重要(Why Immutability Is Important)
在handleClick里面,用了一个slice()
方法把原来的数组克隆出来。有效防止了数组被破坏。
“不突变的对象”这是一个重要的概念,值得React文档重开一章来强调。
有两种改变数据的办法,一个是直接改变(突变,mutate),一种是存到一个变量里面。二者的结果是相同,但是后者有额外的好处。
跟踪变化
查找一个突变对象(mutate)的数据变化是非常麻烦的。 这就要求比较当前对象之前的副本,还要遍历整个对象树,比较每个变量和价值。 这个过程变得越来越复杂。
而确定一个不突变的对象的数据变化相当容易。 如果这个对象和之前相比不同,那么数据就已改变了。就这么简单。
决定React何时重新渲染
最大的好处:在构建简单纯粹的组件时, 因为不突变的数据可以更容易地确定是否更改了,也有助于确定一个组件是否需要被重新渲染。
功能组件
回到之前的项目,现在你不再需要Square
组件中的构造函数了。 事实上,对于一个简单而无状态的功能性组件类型,比如Square
,一个渲染方法足够了,它只干一件事:根据上面传下来的props来决定渲染什么,怎么渲染,完全没必要再开一个扩展组件。
var Square=React.createClass({
render:function(){
return (
<button className="square" onClick={() => this.props.onClick()}>{this.props.value}</button>
);
}
});
可以说这个square组件做到这里就完结了。不用再理他了。
决定次序
目前这个App最大的问题就是整个游戏竟然只有X玩家,简直不能忍,还能不能好好的OOXX了?
对这个App来说谁先走肯定是状态。这个状态决定handleClick渲染的是X还是O:
首先,我们定义X
玩家先走。
var Board=React.createClass({
getInitialState:function(){
return {
squares:Array(9).fill(null),
turnToX:true//为ture时轮到X走
}
},
...
每点击一次,将造成这个开关的轮换。
handleClick:function(i){
var squares=this.state.squares.slice();
squares[i]=this.state.turnToX?'X':'O';
this.setState({
squares:squares,
turnToX:!this.state.turnToX
})
},
现在棋是走起来了。
判断胜负
鉴于井字棋很简单,获胜的最终画面只有8个。所以判断胜负用穷举法就可以了。也就是说,当squares
数组出现8个情况,就宣告胜者并终止游戏。这里妨自己写写判断胜负的引擎:
function judgeWinner(square){
var win=[
[0,1,2],
[0,3,6],
[0,4,8],
[1,4,7],
[2,5,8],
[2,4,6],
[3,4,5],
[6,7,8]
];
for(var i=0;i<win.length;i++){
var winCase=win[i];
if(squares[winCase[0]]==squares[winCase[1]]&&squares[winCase[1]]==squares[winCase[2]]){//三子一线
return squares(winCase[0]);//返回胜利一方的标识
}
}
return false;
}
这个方法在Board渲染前执行就可以了。
...
render:function(){
var winner=judgeWinner(this.state.squares);//每次渲染都判断获胜者
var status='';
if(winner!==null){
status='获胜方是:'+winner
}else{
var player=this.state.turnToX?'X':'O';
status='轮到'+player+'走'
}
return (
<div clasName="board">
<div className="status">{status}</div>
...
好啦!现在已经把这游戏给做出来了。你可以在电脑上自己跟自己下井字棋,一个React新手,走到这一步已是winner。来看看效果吧~
什么,真要完了吗?还有一半的内容。
储存历史步数
现在我们尝试做一个历史步数管理。方便你悔棋或复盘(井字棋还得复盘?!)
每走一步,就刷新一次状态,那就把这个状态存到一个数组对象(比如history
)中。调用这个历史对象的是Game
组件,要做这一步,就得把状态进一步往上放(满满的都是套路啊)。
在Game当中设置状态也是一个大工程。但是基本上和在Board里写状态差不多。
- 首先,用一个history状态存放每一步生成的squares数组。turnToX也提到Game组件中。
- 找出最新的状态
history[history.length-1]
(lastHistory
) - 在handleClick方法中添加落子判断:胜负已分或是已经落子则不响应。
- 在Game渲染函数中写好status,然后放到指定位置。
- 把handleClick函数传到Board组件去!
var Game=React.createClass({
getInitialState:function(){
return {
history:[
{squares:Array(9).fill(null)}
],
turnToX:true
}
},
handleClick:function(i){//这里的i是棋盘的点位。
var history=this.state.history;
var lastHistory=history[history.length-1];
var winner=judgeWinner(lastHistory.squares);
var squares=lastHistory.squares.slice();
if(winner||squares[i]){//如果胜负已分,或者该位置已经落子,则不会响应!
return false;
}
squares[i]=this.state.turnToX?'X':'O';//决定该位置是X还是O
this.setState({
history:history.concat([{squares:squares}]),
turnToX:!this.state.turnToX
});//然后把修改后的squares桥接到状态中去
},
render:function(){
var history=this.state.history;
var lastHistory=history[history.length-1];
var winner=judgeWinner(lastHistory.squares);
var status='';
if(winner){
status='获胜方是'+winner;
}else{
var player=this.state.turnToX?'X':'O';
status='轮到'+player+'走';
}
return (
<div className="game">
<Board lastHistory={lastHistory.squares} onClick={(i)=>this.handleClick(i)} />
<div className="info">
<div>{status}</div>
<ol></ol>
</div>
</div>
);
}
});
那么Board组件里面的各种状态完全不需要了,只保留render和renderSquare函数足矣。
var Board=React.createClass({
renderSquare:function(i){
return <Square value={this.props.lastHistory[i]} onClick={() => this.props.onClick(i)} />
},
render:function(){
return (
<div clasName="board">
<div className="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>
);
}
});
展示历史步数
在之前入门学习中已经有了深刻体会:渲染一串React元素,最好用的方法是数组。
恰好我们的history也是一个数组。而且Game的架构设计中还有一个ol——那么会做了吧?
...
var arr=[];
var _this=this;
history.map(function(step,move){
var content='';
if(move){
content='Move#'+move;
}else{
content='游戏开始~';
}
arr.push(<li key={move}><a onClick={()=>_this.jumpTo(move)} href="javascript:;">{content}</a></li>);
});
...
在这个a标记里,还加了个this.jumpToMove
。当点击之后j将把该索引值的旧状态作为最后一个状态。
好了,现在话分两头,插播一段关于Key值的论述。
论Key值的重要性
任何一个数组,都必须有key值。
当你渲染一串组件,React总是会把一些信息安置到每个单独组件里头去。比如你渲染一串涉及state的组件时,这个state是得存起来的。不管你如何实现你的组件。React都会在背后存一个参照。
你对这些组件增删改查。React通过这些参照信息得知哪些数据需要发生变动。
...
<li>苏三说:xxx</li>
<li>李四说:ooo</li>
..
如上你想修改li的内容,React无法判断哪个li是苏三的,哪个li是李四的。这时就要一个key值(字符串)。对于同辈元素,key是唯一的。
<li key="苏三">苏三说:xxx</li>
<li key="李四">李四说:OOO</li>
key
值是React保留的一个特殊属性,它拥有比ref
更先进的特性。当创建一个元素,React直接把一个key值传到被return的元素中去。尽管看起来也是props之一,但是this.props.key
这样的查询是无效的。
重新渲染一串组件,React通过key来查找需要渲染的匹配元素。可以这么说,key被添加到数组,那这个组件就创建了;key被移除,组件就被销毁。key就是每个组件的身份标志,在重新渲染的时候就可以保持状态。倘若你改变一个组件的key,它将完全销毁,并重新创建一个新的状态。
因此:强制要求你插入到页面的数组元素有key,如果你不方便插入,那么一定是你的设计出了问题。
来场说走就走的时间旅行
由于添加了悔棋这一设定,而悔棋是不可预测的。所以井字棋组件初始需要多一个状态:stepNumber:0。另一方面,悔棋导致turnToX需要重新设定。
jumpTo:function(step){
this.setState({
stepNumber:step,
turnToX:step%2?false:true
})
},
留意到this.state.stepNumber
其实可以取代history.length-1
——那就在render方法和handleClick方法中全部把它替换了。
最后一个问题还是出在handleClick,虽然可以回退,但是状态最终不能实时更新。用history=history.slice(0,this.state.stepNumber+1);
把它剪切一下就行了。
那么全部功能就完成了。嗯,应该是完成了。
var Game=React.createClass({
getInitialState:function(){
return {
history:[
{squares:Array(9).fill(null)}
],
turnToX:true,
stepNumber:0
}
},
handleClick:function(i){
var history=this.state.history;
history=history.slice(0,this.state.stepNumber+1);
var lastHistory=history[this.state.stepNumber];
var winner=judgeWinner(lastHistory.squares);
var squares=lastHistory.squares.slice();
if(winner||squares[i]){
return false;
}
squares[i]=this.state.turnToX?'X':'O';
this.setState({
history:history.concat([{squares:squares}]),
turnToX:!this.state.turnToX,
stepNumber:history.length
});
console.log(this.state.history)
},
jumpTo:function(step){
this.setState({
stepNumber:step,
turnToX:step%2?false:true
});
},
render:function(){
var history=this.state.history;
var lastHistory=history[this.state.stepNumber];
var winner=judgeWinner(lastHistory.squares);
var status='';
if(winner){
status='获胜方是'+winner;
}else{
var player=this.state.turnToX?'X':'O';
status='轮到'+player+'走';
}
var arr=[];
var _this=this;
history.map(function(step,move){
var content='';
if(move){
content='Move#'+move;
}else{
content='游戏开始~';
}
arr.push(<li key={move}><a onClick={()=>_this.jumpTo(move)} href="javascript:;">{content}</a></li>);
});
return (
<div className="game">
<Board lastHistory={lastHistory.squares} onClick={(i)=>this.handleClick(i)} />
<div className="info">
<div>{status}</div>
<ol>{arr}</ol>
</div>
</div>
);
}
});
var Board=React.createClass({
renderSquare:function(i){
return <Square value={this.props.lastHistory[i]} onClick={() => this.props.onClick(i)} />
},
render:function(){
return (
<div clasName="board">
<div className="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>
);
}
});
var Square=React.createClass({
render:function(){
return (
<button className="square" onClick={() => this.props.onClick()}>{this.props.value}</button>
);
}
});
ReactDOM.render(
<Game />,
document.getElementById('container')
);
/**********************************/
function judgeWinner(squares){
var win=[
[0,1,2],
[0,3,6],
[0,4,8],
[1,4,7],
[2,5,8],
[2,4,6],
[3,4,5],
[6,7,8]
];
for(var i=0;i<win.length;i++){
var winCase=win[i];
if(squares[winCase[0]]==squares[winCase[1]]&&squares[winCase[1]]==squares[winCase[2]]){//三子一线
return squares[winCase[0]];//返回胜利一方的标识
}
}
return null;
}
效果如下:
结束的升华
到目前为止,实现了一个井字棋游戏,有了以下基本功能
- 你可以自己跟自己玩井字过三关
- 判断谁赢了
- 记录棋局
- 还允许悔棋
挺好,挺好。
但是,你还可以改进:
- 通过(X,Y)来取代数字坐标
- 对右方的被选中的当前记录进行加粗显示
- 用两个循环重写Board组件,替代掉原来生硬的代码结构
- 对你的历史记录进行升降序排列
- 高亮显示获胜的结果
- 加个人工智能什么的。
这些内容本系列笔记的第2第3篇。
React的井字过三关(1)的更多相关文章
- React的井字过三关(2)
这篇笔记是官方教程的延续笔记,所有代码基于第一篇笔记的结尾代码.旨在解决教程后面提出的五个问题. 一 . 用(X,Y)来取代原有的数字坐标 原来的数字坐标是这样的: 现在的要求是把原来的代码坐标改为二 ...
- React的井字过三关(3)
这是React井字棋项目的最后一篇笔记,记述AI实现. 一. 是开头都会说的原理 但凡懂一点围棋的人都知道"大场"这个概念,可以浅显地把它理解为布局时棋盘上各处的要点.棋谚&quo ...
- [HTML5实现人工智能]小游戏《井字棋》发布,据说IQ上200才能赢
一,什么是TicTacToe(井字棋) 本 游戏 为在下用lufylegend开发的第二款小游戏.此游戏是大家想必大家小时候都玩过,因为玩它很简单,只需要一张草稿纸和一只笔就能开始游戏,所以广受儿 ...
- 『HTML5实现人工智能』小游戏《井字棋》发布,据说IQ上200才能赢【算法&代码讲解+资源打包下载】
一,什么是TicTacToe(井字棋) 本游戏为在下用lufylegend开发的第二款小游戏.此游戏是大家想必大家小时候都玩过,因为玩它很简单,只需要一张草稿纸和一只笔就能开始游戏,所以广受儿童欢迎. ...
- Pascal小游戏 井字棋
一个很经典的井字棋游戏 Pascal源码Chaobs奉上 注意:1.有的FP版本不支持汉语,将会出现乱码.2.别想赢电脑了,平手不错了. 井字过三关: program TicTacToe; uses ...
- 使用 Vue.js 改写 React 的官方教程井字棋
React 的官方教程井字棋很好的引导初学者一步步走进 React 的世界,我想类似的教程对 Vue.js 的初学者应该也会有启发,于是使用 Vue.js 进行了改写 可以先查看最终的结果,尝试点击体 ...
- [CareerCup] 17.2 Tic Tac Toe 井字棋游戏
17.2 Design an algorithm to figure out if someone has won a game oftic-tac-toe. 这道题让我们判断玩家是否能赢井字棋游戏, ...
- quick cocos2d-x 入门---井字棋
学习quick cocos2d-x 第二天 ,使用quick-x 做了一个井字棋游戏 . 我假设读者已经 http://wiki.quick-x.com/doku.php?id=zh_cn阅读了这个链 ...
- 程序设计入门—Java语言 第五周编程题 2井字棋(5分)
2 井字棋(5分) 题目内容: 嗯,就是视频里说的那个井字棋.视频里说了它的基本思路,现在,需要你把它全部实现出来啦. 你的程序先要读入一个整数n,范围是[3,100],这表示井字棋棋盘的边长.比如n ...
随机推荐
- html img图片等比例缩放
在img标签里面只设置宽,不设置高,图片就会等比例缩放.
- Java中堆内存和栈内存详解2
Java中堆内存和栈内存详解 Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,ja ...
- Mac下安装Matlab R2015b
步骤 下载Matlab R2015b链接:http://pan.baidu.com/s/1pLGpUfp 密码:k1xg 下载破解文件r2015b_crackhttp://download.csdn. ...
- HTML---用记事本写html
<DOCTYPE HTML> <--DOCTYPE 文档类型,浏览器按照该类型解析--> <html> <head> <title>这个是h ...
- iOS系列 基础篇 03 探究应用生命周期
iOS系列 基础篇 03 探究应用生命周期 目录: 1. 非运行状态 - 应用启动场景 2. 点击Home键 - 应用退出场景 3. 挂起重新运行场景 4. 内存清除 - 应用终止场景 5. 结尾 本 ...
- properties属性文件工具类
package xxx.business.utils; import org.apache.commons.configuration.ConfigurationException; import o ...
- Java 加解密 AES DES TripleDes
package xxx.common.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.crypt ...
- Centos 6.5 Percona 5.6.27 Tokudb 配置
参考 https://www.percona.com/doc/percona-server/5.6/tokudb/tokudb_installation.html # wget https://www ...
- oracle 自增长序列
create or replace TRIGGER "METTINGUSER".TRG_PREPN BEFORE INSERT ON "PREPROJFUN" ...
- 特殊文件: /dev/null和/dev/tty
转自:http://www.cnblogs.com/stephen-liu74/archive/2011/11/10/2240461.html Linux系统提供了两个对Shell编程非常有用的特殊文 ...