使用 Vue.js 改写 React 的官方教程井字棋
https://zh-hans.reactjs.org/tutorial/tutorial.html
React 的官方教程井字棋很好的引导初学者一步步走进 React 的世界,我想类似的教程对 Vue.js 的初学者应该也会有启发,于是使用 Vue.js 进行了改写
可以先查看最终的结果,尝试点击体验,我们将逐步地实现这个效果
https://codepen.io/chanvin/pen/rNVZwJy?editors=0010
初始状态代码
初始状态查看
打开初始状态直接编辑,或者将对应的文件复制下来放置在同一文件夹中
https://codepen.io/chanvin/pen/yLNxVdL
此时只是一个简单的井字棋格子,以及写死的下一个选手
初始代码分析
目前定义了三个组件,分别为 Square,Board 和 Game
Square 目前只是一个普通的按钮
1 |
Vue.component('Square', { |
这样定义了组件后,别的组件就可以直接以 <Square /> 的方式引用该组件
Board 模版由当前状态和 9 个 Square 组成
1 |
Vue.component('Board', { |
data 定义了当前状态 status,和 board 的值,这样在模版中就可以用 的方式引用状态值,使用 v-for 将 board 二维数组里的值两次循环组装成井字格
在组件中的 data 必须是返回对象的函数而非对象字面值
v-for 需要有 key 确保性能以及不报警告
Game 模版由 Board 与 后面需要增加的状态和历史组成
1 |
Vue.component('Game', { |
增加数据处理
增加 props
在 Board 中传递一个名为 value 的 prop 到 Square
1 |
<Square v-for="square in row" :key="square" :value="square" /> |
:value 是 v-bind:value 的缩写,表示其值是一个表达式
在 Square 的组件定义和模版中增加 value prop
1 |
Vue.component('Square', { |
props 为父组件可传递给子组件的变量,在父组件调用子组件时在标签中设置对应属性,在子组件中使用方法与 data 一致
目前的代码和效果:0 - 8 的数字分别填充进井字棋格中
https://codepen.io/chanvin/pen/wvaEgPV?editors=0010
增加交互
增加点击事件至按钮元素以更新值
1 |
Vue.component('Square', { |
@click 为 v-on:click 的缩写,其值为点击需要运行的函数,这里为组件定义的方法 methods 中的 setValue
子组件不能直接更新父组件的值,所以将 value 从 props 改为 data
data 的值更新,对应模版就会自动更新展示内容
目前的代码和效果:点击井字棋格,对应填充 X
https://codepen.io/chanvin/pen/jOPvyxW?editors=0010
完善游戏
数值提升
为交替落子和确认输赢,需要统一判断各格状态,所以将 value 提升至 Board
Board 增加数据 squares 和方法 handleClick
1 |
Vue.component('Board', { |
squares 初始为 9 个 null 组成的数组,井字棋盘为空的状态
handleClick 接收对应格子序号的参数,并更新对应的 squares 元素
事件触发的处理函数不是 handleClick(square) 的返回值,而是 handleClick,只是在触发时会带上参数值 square
在 Square 的点击事件处理器中触发 Board 的点击事件
1 |
Vue.component('Square', { |
value 要从 data 改回到 props
$emit 可以触发父组件传递的事件
prop 里的值在父组件更新,子组件模版也会对应更新展示内容
目前的代码和效果:点击井字棋格,如果未被占,则填充 X
https://codepen.io/chanvin/pen/mdJGWxV?editors=0010
轮流落子
增加数据 xIsNext,并在点击时切换
1 |
data() { |
xIsNext 初始值为 true,即 X 先落子
点击后,通过取反交替 xIsNext
更新状态值 status 为下一个落子者
目前的代码和效果:点击井字棋格,X 和 O 交替落子
https://codepen.io/chanvin/pen/ZEGMKBZ?editors=0010
判断胜者
增加计算胜者的函数
1 |
function calculateWinner(squares) { |
列举可能获胜的组合,与 squares 数组的值进行比对
增加点击处理函数的胜者逻辑
1 |
if (calculateWinner(squares)) { |
点击后,如果之前已有取胜,则点击无效
处理落子后,再次判断是否取胜,更新状态
目前的代码和效果:有一方获胜后, 状态和点击处理更新
https://codepen.io/chanvin/pen/PoqdmEQ?editors=0010
增加事件旅行
保存历史记录
为实现“悔棋”功能,需要记录每一次落子的整体状态,相当于棋盘的快照,作为一个历史记录,提升至 Game 组件中
在 Game 增加数据 history,将 xIsNext,status 和 handleClick 方法 从 Board 中转移到 Game 中
1 |
Vue.component('Game', { |
squares 从 history 的最后一个记录取值(目前只有一个记录)
落子后,squares 把落子记录进去后,history 再增加一个记录
Board 增加 prop squares,handleClick 更新为触发父组件的事件
1 |
Vue.component('Board', { |
目前的代码和效果:状态位置更新,历史记录已存储
https://codepen.io/chanvin/pen/poJOwbv?editors=0010
展示历史步骤记录
把历史记录循环展示出来,并绑定点击事件,通过 stepNumber 的更新显示对应步骤的记录
1 |
Vue.component('Game', { |
在 Game 中增加 stepNumber,初始为 0,记录当前展示的步骤
将 Board 的 prop squares 的取值更新为 this.stepNumber 对应的步骤
handleClick 中以已当前步骤为基础处理 history,并更新 stepNumber
增加方法 jumpTo 处理回到历史的展示,更新 stepNumber,xIsNext 和 status
最终的代码和效果:每落一子,都会增加一个历史步骤,点击步骤可回到该步
https://codepen.io/chanvin/pen/rNVZwJy?editors=0010
总结
游戏实现内容
交替落子
判断输赢
悔棋重来
展示技术内容
v-bind 在模版中进行数据绑定
v-for 在模版中进行数组循环
v-on 在模版中进行事件传递和触发
data 在组件的定义和模版自动更新
prop 在组件的传递和模版自动更新