使用react结合Immutable实现一道五子棋面试题(上)

引言

一道朋友的面试题,要求如下:

  • 用web技术实现一个五子棋
  • 支持DOM和Canvas版本切换
  • 实现悔棋功能
  • 实现一个撤销悔棋功能

看起来挺有意思的,便用react redux Immutable等技术自己实现了一个。
github: https://github.com/fenggu/gomoku

Store数据结构

因为要支持DOM和Canvas版本切换的切换,所以采用react这种单向数据流的框架再适合不过了,只需要让DOM和Canvas版本公用一套数据流,然后做好View层的显示就好了。所以首先我们先来定义我们的store
store对象结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
gomoku: [
[
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
],
index: 0, // 对局索引
win: { // 胜利状态
role: 0, // 胜利对象
size: null // 胜利时的棋盘数组长度
}

其中棋盘上的棋子数据以二维数组pieces[x][y]:value的形式存储,
0代表空,1代表白,2代表黑。
store里的gomoku将每一步的“棋盘数据”都存储进数组里,并通过Immutable的API:gomoku.get(index)的形式读取。所以我们将每一步的序列索引index也存进store中,这样也极大的方便开发时间旅行功能。
因为随着下棋步数越来越多,gomoku里存储的数组也会越来越大。为了解决这个问题,一般的做法是使用 shallowCopy(浅拷贝)或 deepCopy(深拷贝)来避免被修改,但这样做造成了 CPU 和内存的浪费。
Immutable 可以很好地解决这些问题。Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗。

DOM版开发

快速搭建完基本框架后,就可以进行愉快的DOM版本开发了。(基本框架代码参考顶部项目GitHub)

绘制棋盘

通过了解五子棋棋盘是15x15的格子布局,所以我采用div堆叠的方式来绘制这个棋盘。

1
2
3
4
5
6
7
8
9
renderBox = () => {
let boxs = []
for (let i = 0; i < 14; i ++) {
for (let j = 0; j < 14; j++) {
boxs.push(<div key={`b-${i}-${j}`} className='gomoku-box'></div>)
}
}
return boxs
}

绘制棋子

然后是棋子的布置,我们知道五子棋是下在棋盘线路的交叉点上,因此这里我采用16x16个透明格子的形式将棋子的格子铺在棋盘上,通过错位而达到棋子定位的效果。这样同时也方便了点击事件绑定。

1
2
3
4
5
6
7
8
9
renderPieces = () => {
let boxs = []
for (let i = 0; i < 15; i ++) {
for (let j = 0; j < 15; j++) {
boxs.push(<Piece key={`${i}-${j}`} x={i} y={j} isWin={this.props.isWin} class='gomoku-box'></Piece>)
}
}
return (boxs)
}

render

1
2
3
4
5
6
7
8
9
10
11
12
render() {
return (
<div className="gomoku">
<div className="gomoku-pieces">
{ this.renderPieces() }
</div>
<div className="gomoku-back">
{ this.renderBox() }
</div>
</div>
);
}

棋子组件

每一个棋子都是一个Piece小组件,Piece接受x y两个参数用来确定棋子的位置,在触发点击事件的同时,会将x y参数组成一个新的棋盘,并且通过dispatch action来更新棋盘的数据。

1
2
3
4
5
6
7
8
9
10
11
goMoku = () => {
// 黑: 2, 白: 1, 空: 0
let x = this.props.x
let y = this.props.y
if ( this.props.gomoku.get(x).get(y) !== 0 || this.props.win.role !== 0) return
let isCurrent = this.props.index % 2 === 0 ? 2 : 1
let newGoMoku = this.props.gomoku.setIn([x, y], isCurrent)
this.props.actions.pushGoMoku(newGoMoku, this.props.index + 1)
this.props.actions.changeGoMokuIndex(this.props.index + 1)
this.props.isWin(x, y, isCurrent)
}

同时为了防止棋子的重复渲染我们在PiececomponentWillReceiveProps中加入条件判断。

1
2
3
4
5
6
7
componentWillReceiveProps (nextProps) {
let x = this.props.x
let y = this.props.y
if (nextProps.gomoku.get(x).get(y) == this.props.gomoku.get(x).get(y)) {
return false
}
}

效果图:
img
下文: 使用react结合Immutable实现一道五子棋面试题(下)

投食二维码