今天笔者来实习一个非常简单的功能,也就是大家都爱玩的井字棋,本次笔者参照了react官网的新手教程,会从0到1进行js代码的实现(css大家可以自己自由发挥)。个人认为这个例子帮助到了笔者很多,在做例子的时候遇到不会的地方都有看文档然后写对应的文章来进行记录
总结
总的来说这个让我学会了
- 组件的使用,分工要明确,使得维护就比较容易
- 数据为什么要传在最高级的父组件上
- key
- 不可变性
- state的具体用法
- props的流出
功能设想
- 点击后会交替出现X,O
- 历史记录功能
- 优胜者判断
-
声明下一个棋子是X还是O
完成后的预览
手撕代码
开始写之前,我们要对整体项目有个大概的预估,大概需要哪几个组件?分别对应了什么功能?
笔者认为需要三个组件,一个用来渲染方格内的变化,一个用来渲染整体九宫格,一个用来存放游戏内部逻辑,因此我们可以整如下三个组件
组件A,Square
class Square extends React.Component {
/*存放方格内变化*/
}
组件B,board
class Board extends React.Component {
/*存放九宫格的变化*/
}
组件C,game
class Game extends React.Component {
/*存放内部逻辑*/
}
简单搭构
建立好了组件,我们便可以对其中一些比较简单的功能进行实现和搭构,包括让每个方格都变成按钮可以接受事件,渲染出九宫格,这里比较简单就不赘述了,其中css代码就不在这赘述
class Square extends React.Component {
render() {
return (
<button className="square">
{/* value */}
</button>
);
}
}
class Board extends React.Component {
renderSquare(i) {
return <Square />;
}
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 {
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{}</div>
<ol>{}</ol>
</div>
</div>
);
}
}
![](https://img.haomeiwen.com/i19247518/a36c33d3a5d9fdbd.png)
数据处理?
像新手都会本能有一个误区,认为数据在哪里显示渲染,数据就要存在哪里,实则这是一个非常错误的思想。笔者认为数据应该储存在内部逻辑中,也就是最高级的状态里,这样有几个好处
- 不用重复储存,如果仅仅存在最低级的状态中,每次其他更高级的状态要调用都要重新储存
- 如果写在底层的逻辑,虽然代码是可行,但是每次维护修改都特别的麻烦
所以不用想,应该把你的数据放在你最高级的父组件中,在这也就是Game组件,通常我们会使用构造函数去定义组件的属性
constructor(props) {
super(props)
this.state = {
/*这里写要保存的数据*/
}
}
在react中,组件中的 state 包含了随时可能发生变化的数据。state 由用户自定义,它是一个普通 JavaScript 对象。
如果某些值未用于渲染或数据流(例如,计时器 ID),则不必将其设置为 state。此类值可以在组件实例上定义。具体可以看我另外一篇文章state的具体用法
我们完成项目的思维应该是先一个一个完成功能,如果完成功能间会有冲突再进行更新修改
我们完成项目的思维应该是先一个一个完成功能,如果完成功能间会有冲突再进行更新修改
我们完成项目的思维应该是先一个一个完成功能,如果完成功能间会有冲突再进行更新修改
所以当我们需要确定数据的时候,我们来康康要完成第一个功能时,我们需要做什么?
点击后会交替出现X,O
我们要首先明白,点击后交替出现X,O是改变了什么?渲染的时候要渲染什么?然后每一个组件负责要做什么
其实比较清晰的是,square组件要做的就是接受父组件board组件传递出来的数据并且渲染出来,所以board组件要做的就是传递出合适的props给square渲染,相当于一个中间的过渡。而Game则是要传递出正确的逻辑给board组件,使其可以正确的传给square,因此两个子组件的写法就比较简单,先看board组件
class Board extends React.Component {
renderSquare(i) {
return <Square value={/*要看看game存的数据叫啥*/}
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>
);
}
}
首先board要做的事情非常简单,game将会传递一个props给board,里面包含了要渲染的数据,同时也会传递一个关于onClick函数的props给它,所以它把这些给square就好了
class Square extends React.Component {
render(props) {
return (
<button className="square"
onClick={this.props.onClick}>
{this.props.value}
</button>
);
}
}
square组件渲染出board组件给的props就好
可以发现这样把数据存在父组件中,子组件的搭建就非常简单
接下来我们看看最难的game组件,要怎样做
class Game extends React.Component {
constructor(props) {
super(props)
this.state = {
squares: Array(9).fill(null)
},
xNext: true,
}
}
handleClick=(i)=> {
const newSquares = this.state.squares.slice()
newSquares[i] = this.state.xNext ? 'X' : 'O'
this.setState({
squares: newSquares,
xNext: !this.state.xNext,
})
}
render() {
return (
<div className="game">
<div className="game-board">
<Board
onClick={this.handleClick}//升级版写法
squares={this.state.squares} />
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}
其实要做的有三个,一个是储存数据,一个是定义一个方法,然后是渲染时把board需要的props传出去,要注意的是Setstate的时候最好使用函数,因为比如这里
this.setState({
squares: newSquares,
xNext: !this.state.xNext,
})
console.log(this.state.squares)
当你console.log出来的时候会发现,其永远是显示上一个的 squares,这是因为setState是异步函数,所以是执行完console才执行,一般牛逼的都用里面加函数
储存数据我们可以弄一个长度为9的空数组储存每次点击的数据,通过使用 .slice()
方法创建了数组的一个副本,而不是直接修改现有的数组。这是由于react中不可变性很重要。可以看看这篇关于不可变性的文章
然后整一个关于状态的属性来控制输出X还是O
定义方法要做的就是每次点击会调用什么方法,来实现功能。这个很简单不赘述了,然后渲染出去就欧克了
然后把上面注释的地方改成传出去props的变量名就ok了
历史记录功能
历史记录功能其实最为要关注的就是game,因为子组件只需要渲染出给的东西就ok了,所以不用怎么变动
因为要记录历史,所以我们要先对储存数据增加一个history的属性,同时还要把走到第几步记下来。
constructor(props) {
super(props)
this.state = {
history: [
{
squares: Array(9).fill(null)
}
],
xNext: true,
stepNumber: 0
}
}
所以我们改变了构造函数成了这样,其实逻辑就是让history不止记下一次的操作和值,会每次都改变原来的history数组,所以我们后面也选择了concat方法而不是push,正是这个原因。
然后我们康康方法的变化
首先是原来的handleClick方法
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1)
const current = history[history.length - 1]
const newSquares = current.squares.slice()
newSquares[i] = this.state.xNext ? 'X' : 'O'
this.setState({
history: history.concat([{
squares: newSquares
}]),
xNext: !this.state.xNext,
stepNumber: history.length
})
}
遵从数据的不可变性,我们声明了一个新的history来记录从开始到进行的这一步,这个方法的变化逻辑也很好理解看看码就行了
然后是jumpto的方法
jumpto(step) {
this.setState(
{
stepNumber: step,
xNext: (step % 2) === 0
}
)
}
实际上就是改变stepNumber使得回到那一步
最后是渲染
render() {
const history = this.state.history
const current = history[this.state.stepNumber]
const winner = calculateWinner(current.squares)
const moves = history.map((step, move) => {
const historyMove = move ?
'go to move#' + move :
`go to Game start`
return (
<li key={move}>
<button onClick={() => this.jumpto(move)}>
{historyMove}
</button>
</li>
)
})
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
onClick={(i) => { this.handleClick(i) }}
squares={current.squares} />
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}
这里需要重点提一下的是<li key={move}>
,key的重要性可以去看看我的文章key
,什么时候要用什么时候不用为什么要用都讲的很清楚了。
同时这里的move是相当于对数组的index,关于map的用法mdn给出了一个示例
let new_array = arr .map(function callback(currentValue [,index [,array ]]){
//返回new_array的元素
} [,thisArg ])
所以我们选择了move作为key,因为其不会重复,独一无二
其实相对而言其他逻辑都比较简单就不赘述了
优胜者判断
这里要整一个判断的函数
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;
}
实际上这个判断逻辑也很容易,就是把获胜可能性列出来,然后判断存不存在即可。
其他的就是把源码补充一下就好
井字棋源码
网友评论