數據驅動是 Vue 框架的核心特性之一,也是 Vue 響應式原理的具體體現,相信大家對其應該深有體會,尤其是在操作數據來觸發頁面更新的時候。
為了讓大家更加了解數據驅動的理念,并解決使用過程中可能出現的一系列問題,本文將結合比較常見和簡單的 “拼圖游戲” 來展示 Vue 數據驅動的魅力所在。
## 效果展示
首先我們先來看一下實現的 “拼圖游戲” 的動態效果:

在不操作 `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:嘗試使用外部數據
- 總結篇:寫在最后