ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
数据驱动是 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 的整数?