数据驱动是 Vue 框架的核心特性之一,也是 Vue 响应式原理的具体体现,相信大家对其应该深有体会,尤其是在操作数据来触发页面更新的时候。
为了让大家更加了解数据驱动的理念,并解决使用过程中可能出现的一系列问题,本文将结合比较常见和简单的 “拼图游戏” 来展示 Vue 数据驱动的魅力所在。
## 效果展示
首先我们先来看一下实现的 “拼图游戏” 的动态效果:
![](https://img.kancloud.cn/a5/7f/a57f4c797e4e005a6f8570e28fccbdde_360x240.gif)
在不操作 `DOM` 的情况下实现以上功能其实需要我们对 Vue 数据驱动及数据可视化有一个非常清楚的认知,在操作数据的同时驱动可视化界面的还原。
## 关键代码
接下来我们来看一下实现该拼图游戏的功能点及关键代码:
### 游戏面板的构建
```
<!-- HTML 部分 -->
<ul class="puzzle-wrap">
<li
:class="{'puzzle': true, 'puzzle-empty': !puzzle}"
v-for="(puzzle, index) in puzzles"
:key="index"
v-text="puzzle"
></li>
</ul>
```
```
// 数据部分
export default {
data() {
return {
puzzles: Array.from({ length: 15 }, (value, index) => index + 1)
}
},
}
```
上方我们使用 `v-for` 循环构建了从 1 ~ 15 按顺序排列的方块格子,也就是拼图完成时候的顺序,但是拼图游戏一开始数字的顺序应该是无序的,也是随机打乱的,那么我们怎么实现呢?可以使用下方的随机排列函数:
```
function shuffle(arr) {
let len = arr.length
for (let i = 0; i < len - 1; i++) {
let idx = Math.floor(Math.random() * (len - i))
let temp = arr[idx]
arr[idx] = arr[len - i - 1]
arr[len - i - 1] = temp
}
return arr
}
```
该函数中我们使用 `Math.random()` 来返回 0 和 1 之间的伪随机数,可能为 0,但总是小于1,\[0, 1),而通过这一特性我们可以实现生成 n-m,包含 n 但不包含 m 的整数,具体步骤如下:
* 第一步算出 `m-n` 的值,假设等于 w
* 第二步 `Math.random() * w`
* 第三步 `Math.random() * w + n`
* 第四步 `Math.floor(Math.random() * w + n)`
在 `shuffle` 函数中 n 值永远是 0,而 w(即 len - i) 值随着循环 i 值的变大而不断减小。
> 在上面的算法里,我们每一次循环从前 len - i 个元素里随机一个位置,将这个元素和第 len - i 个元素进行交换,迭代直到 i = len - 1 为止。
这一便实现了数组的随机打乱。最后我们需要在数组末尾追加一个空值来显示唯一一个空白格子:
```
this.puzzles.push('');
```
### 交换方块位置
实现随机数字后,当我们点击方块,如果其上下左右存在为空的格子就需要将其进行交换,而由于是数据驱动界面,这里我们便需要交换两者在数组中的位置来实现:
```
export default {
methods: {
// 点击方块
moveFn(index) {
let puzzles = this.puzzles
// 获取点击位置上下左右的值
let leftNum = this.puzzles[index - 1],
rightNum = this.puzzles[index + 1],
topNum = this.puzzles[index - 4],
bottomNum = this.puzzles[index + 4]
// 和为空的位置交换数值
if (leftNum === '' && index % 4) {
this.setPuzzle(index, -1)
} else if (rightNum === '' && 3 !== index % 4) {
this.setPuzzle(index, 1)
} else if (topNum === '') {
this.setPuzzle(index, -4)
} else if (bottomNum === '') {
this.setPuzzle(index, 4)
}
},
// 设置数组值
setPuzzle(index, num) {
let curNum = this.puzzles[index]
this.$set(this.puzzles, index + num, curNum)
this.$set(this.puzzles, index, '')
},
}
}
```
由于是 16 宫格的拼图,所以我们在点击获取位置的时候需要考虑边界情况,比如第 4 个格子为空,我们点击第 5 个格子不应该交换它们,因为在界面上第 4 个格子不在第 5 个格子的左侧,所以我们使用 `index % 4` 的方法来进行边界的判断,同时使用 Vue 提供的 `$set` 方法来将响应属性添加到数组上。
### 校验是否过关
最后我们需要校验游戏是否过关,我们只需要在最后一个格子为空时去进行校验即可:
```
if (this.puzzles[15] === '') {
const newPuzzles = this.puzzles.slice(0, 15)
const isPass = newPuzzles.every((e, i) => e === i + 1)
if (isPass) {
alert ('恭喜,闯关成功!')
}
}
```
我们使用数组的 `every` 方法来简化代码的复杂度,当所有数字大小和对应的数组下标 + 1 相吻合时即会返回 `true`。
如此我们便完成了一个简单拼图游戏的功能。
## 盲点及误区
在实现拼图游戏后,有些同学可能会存在一些疑惑,比如:数组赋值为什么要用 $set 方法?数组随机打乱为什么不用 sort 排序呢?下面便来进行讲解:
### 为什么要用 $set 方法
大家应该都知道如果不用 `$set` 方法我们可以直接通过操作数组索引的形式对数组进行赋值,从而交换拼图的中两者的数据:
```
// 设置数组值
setPuzzle(index, num) {
let curNum = this.puzzles[index]
this.puzzles[index + num] = curNum
this.puzzles[index] = ''
// this.$set(this.puzzles, index + num, curNum)
// this.$set(this.puzzles, index, '')
}
```
但是你会发现这样做数据是改变了,但是页面并没有因此重新渲染,这是为什么呢?其实 Vue 官方已经给出了明确的答案:
> 由于 JavaScript 的限制,Vue 不能检测以下变动的数组:
>
> * 当你利用索引直接设置一个项时,例如:vm.items\[indexOfItem\] = newValue
> * 当你修改数组的长度时,例如:vm.items.length = newLength
我们这里使用的便是第一种利用索引的方式,由于 Vue 检测不到数组变动,因此页面便无法重绘。同样 Vue 也不能检测对象属性的添加或删除,需要使用 `Vue.set(object, key, value)` 方法来实现。
其实还有一种比较取巧的方式便是强制重新渲染 Vue 实例来解决这一问题:
```
// 设置数组值
setPuzzle(index, num) {
let curNum = this.puzzles[index]
this.puzzles[index + num] = curNum
this.puzzles[index] = ''
this.$forceUpdate() // 迫使 Vue 实例重新渲染
// this.$set(this.puzzles, index + num, curNum)
// this.$set(this.puzzles, index, '')
}
```
上方我们使用了 Vue 提供的 `$forceUpdate` 方法迫使 Vue 实例重新渲染,这样改变的数据就会被更新的页面中去。但是最好不要这样操作,因为这会导致 Vue 重新遍历此对象所有的属性,一定程度上会影响页面的性能。
### 为什么不用 sort 排序
其实 sort 方法也能够实现数组的随机排序,代码如下:
```
let puzzleArr = Array.from({ length: 15 }, (value, index) => index + 1);
// 随机打乱数组
puzzleArr = puzzleArr.sort(() => {
return Math.random() - 0.5
});
```
我们通过使用 `Math.random()` 的随机数减去 0.5 来返回一个大于、等于或小于 0 的数,sort 方法会根据接收到的值来对相互比较的数据进行升序或是降序排列。
但是由于 JavaScript 内置排序算法的缺陷性,使用 sort 排序的结果并不随机分布,经过大量的测试你会发现**越大的数字出现在越后面的概率越大**。
由于本文并非是一篇介绍 sort 排序的文章,关于论证其缺陷性的话题这里就不进行详细展开了,感兴趣的同学可以进一步进行探究。
## 结语
本文实例是基于我之前写的一篇关于利用 Vue.js 实现拼图游戏的文章上进行了改进和优化,希望通过这样一个小游戏来强化大家对于 Vue 数据驱动的理解。相比操作 DOM 元素,操作数据其实更加的便捷和快速,可以使用较少的代码来实现一些较为复杂的逻辑。
具体实例代码可以参考:[puzzle](https://github.com/luozhihao/vue-project-code/blob/ea7294370af888084be41c10c914b4fedbf3f400/ui-framework-project/src/views/demo/puzzle.vue)
## 思考 & 作业
* Vue 中监听数据变化的原理是什么?是通过何种方式实现的?
* 如何论证原生 JS 中 sort 排序后越大的数字出现在越后面的概率越大?
* 如何使用 `Math.random()` 生成 n-m,不包含 n 但包含 m 的整数?
- 开篇:Vue CLI 3 项目构建基础
- 构建基础篇 1:你需要了解的包管理工具与配置项
- 构建基础篇 2:webpack 在 CLI 3 中的应用
- 构建基础篇 3:env 文件与环境设置
- 构建实战篇 1:单页应用的基本配置
- 构建实战篇 2:使用 pages 构建多页应用
- 构建实战篇 3:多页路由与模板解析
- 构建实战篇 4:项目整合与优化
- 开发指南篇 1:从编码技巧与规范开始
- 开发指南篇 2:学会编写可复用性模块
- 开发指南篇 3:合理划分容器组件与展示组件
- 开发指南篇 4:数据驱动与拼图游戏
- 开发指南篇 5:Vue API 盲点解析
- 开发拓展篇 1:扩充你的开发工具
- 开发拓展篇 2:将 UI 界面交给第三方库
- 开发拓展篇 3:尝试使用外部数据
- 总结篇:写在最后