小弟在開發 Vue 或是 Nuxt 專案的時候,都曾經遇過 v-for
的迭代對象,明明已經被修改了,卻沒有看到畫面的變化,其中有些是 JS 撰寫的一些重點沒注意,有些就真的和 Vue 本身有關。下面就列出幾個曾經犯過的蠢事讓大家笑一笑。
下面是一個包含搜尋功能的 post 修改範例。
<template>
<div>
<div>
Search: <input v-model="keyword"/>
</div>
<li v-for="(post, index) in postsToShow" :key="index">
<span> {{post.title}} </span>
<button @click="editPost(post)">update</button>
</li>
<hr />
<template v-if="editIndex">
<div>Title: <input v-model="tempPost.title" /></div>
<button @click="cancelEdit">cancel</button>
<button @click="updatePost">save</button>
</template>
</div>
</template>
<script>
const simulateAPI = (data) => {
return new Promise((resolve) => {
resolve(data);
});
};
export default {
name: 'test',
data() {
return {
posts: [
{ title: 'Day 01. 話說踩坑前...' },
{ title: 'Day 02. 惱人的環境設定' },
{ title: 'Day 03. Laravel 專案開箱'},
{ title: 'Day 04. DB 三劍客 Migration, Model 和 Resource' },
{ title: 'Day 05. 一不小心就會扯遠的依賴注入 (DI)' },
{ title: 'Day 06. Controller 減重計畫 (Repository 篇)' },
],
keyword: "",
postsToShow: [],
editIndex: undefined,
tempPost: undefined,
}
},
watch: {
keyword(val) {
this.postsToShow = [];
if (val) {
this.posts.forEach((post) => {
if (post.title.includes(this.keyword)) {
this.postsToShow.push(post);
}
});
} else {
this.postsToShow = this.posts;
}
}
},
methods: {
restTempPost() {
this.editIndex = undefined;
this.tempPost = undefined;
},
cancelEdit() {
this.restTempPost();
},
editPost(post) {
this.editIndex = this.posts.indexOf(post);
this.tempPost = { title: post.title };
},
async updatePost() {
const { success } = await simulateAPI({ success: true });
if (success) {
// ****************** 戰犯在這裡 ******************
this.posts[this.editIndex] = this.tempPost;
this.restTempPost();
}
},
},
mounted() {
this.postsToShow = this.posts;
}
}
</script>
在上面的操作畫面當中我們可以看到,當我們儲存了修改,可是畫面並沒有顯示出修改的資料,我們將 this.posts[this.editIndex]
顯示出來之後,確實是有更新,接著再檢查 this.postsToShow[0]
,哪尼!! 竟然是原來的資料!? 這個錯誤,其實跟 v-for
毫無關係,這是 JS 指標觀念的問題,從下面第一張圖是搜尋「l」之後,this.posts[2]]
和 this.postsToShow[0]
都是指向同一個 post,然而圖二是修改並儲存之後,只更改了 this.posts
,所以當然會出問題。
像這樣的錯誤應改為每一個 property 逐一賦值:
// ...
this.posts[this.editIndex].title = this.tempPost.title;
// ...
下面是一個使用者列表,可以透過點擊標頭完整排序,或是點擊按鈕,自行調整位置。
<template>
<div>
<table>
<thead>
<tr>
<th @click="sortBy('id')">ID</th>
<th @click="sortBy('name')">姓名</th>
<th @click="sortBy('phone')">電話</th>
<th @click="sortBy('email')">e-mail</th>
</tr>
</thead>
<tbody>
<tr v-for="(user, index) in users" :key="index">
<td>{{user.id}}</td>
<td>{{user.name}}</td>
<td>{{user.phone}}</td>
<td>{{user.email}}</td>
<td><button v-if="index > 0" @click="move('+', index)">up</button></td>
<td><button v-if="index < users.length - 1" @click="move('-', index)">down</button></td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
name: 'test',
data() {
return {
users: [
{ id: 8, name: '勘吉', phone: '0900000002', email: '789@sport.org'},
{ id: 13, name: '麗子', phone: '0900000001', email: '456@sport.org'},
{ id: 20, name: '大原所長', phone: '0900000004', email: 'def@sport.org'},
{ id: 21, name: '檸檬', phone: '0900000003', email: 'abc@sport.org'},
{ id: 41, name: '中川', phone: '0900000000', email: '123@sport.org'},
],
timestamp: (new Date()).getTime(),
};
},
methods: {
sortBy(column) {
this.users = this.users.sort(function (a, b) {
return a[column] > b[column] ? 1 : -1;
});
},
move(direction, index) {
const switchIndex = (direction === '+') ? index - 1 : index + 1;
[this.users[index], this.users[switchIndex]] =
[this.users[switchIndex], this.users[index]];
},
}
}
</script>
當我們點擊標頭進行排序的時候一切都很正常,但是當我們只各別將某個 user 上移或下移 (呼叫 this.move
) 的時候就出事了!! 可是 this.users
卻是有更改順序的,第一次遇到這個問題的時候真的整個崩潰 (這就是不學無術的後果 QQ)。
根據官網文件說明:
Due to limitations in JavaScript, Vue cannot detect the following changes to an array:
01. When you directly set an item with the index, e.g.vm.items[indexOfItem] = newValue
02. When you modify the length of the array, e.g.vm.items.length = newLength
從上面的範例來看,透過 this.users[index] = this.users[switchIndex]
確實符合無法偵測陣列資料變動的條件。為解決此問題,可以使用官方提供強制重新渲染畫面的方法「this.$forceUpdate();
」。所以範例中的 move()
方法修改如下:
move(direction, index) {
const switchIndex = (direction === '+') ? index - 1 : index + 1;
[this.users[index], this.users[switchIndex]] =
[this.users[switchIndex], this.users[index]];
this.$forceUpdate();
},
修改完的結果:
一切看似 OK ,實際上還沒結束啊! 我們在換個例子,下面是一個草稿文章勾選之後會移動到已發文列表,反之亦然的一個簡單的資料移動練習。
<template>
<div>
<div>
<h3>Draft</h3>
<li v-for="(draft, index) in draftList" :key="index">
<input type="checkbox" @change="postTheDraft(draft, index)" />
<span>{{draft}}</span>
</li>
</div>
<hr />
<div>
<h3>Post</h3>
<li v-for="(post, index) in postList" :key="index">
<input type="checkbox" checked @change="removeThePost(post, index)" />
<span>{{post}}</span>
</li>
</div>
</div>
</template>
<script>
export default {
name: 'test2',
data() {
return {
draftList: [ 'Day 01', 'Day 02', 'Day 03', 'Day 04', 'Day 05' ],
postList: [],
};
},
methods: {
postTheDraft(draft, index) {
this.draftList.splice(index, 1);
this.postList.push(draft);
},
removeThePost(post, index) {
this.postList.splice(index, 1);
this.draftList.push(post);
},
}
}
</script>
然而實際操作的時候卻怪怪的,為甚麼草稿有正常移動到已發文列表,可是草稿列表的項目確有一個被打勾呢?
讓我們再仔細的看一次圖中 dom elements tree 的部份,當我們勾選了草稿項目時,其實 <input type="checkbox">
節點是沒有被更新的 (意思是一開始點擊的 checkbox dom element 還是原來的所以它才會仍然被勾選著)! 回到範例中,可以看到 checkbox 的部分並沒有綁定任何會變動的資料,因此我們可以知道,「當畫面在重新渲染時,只會渲染有綁定資料或是有變動(增加或刪除)的節點」。
要解決這個問題就是讓所有節點資料變動,講白的就是更新 :key
的值,當 :key
一變動,相關的 dom elements 就會重新建立。因此上面的範例改寫之後就可以正常運作了:
<li v-for="(draft, index) in draftList" :key="`draft-${timestamp}-${index}`">
<!-- ... -->
<li v-for="(post, index) in postList" :key="`post-${timestamp}-${index}`">
export default {
data() {
return {
// ...
timestamp: new Date().getTime(),
};
},
methods: {
postTheDraft(draft, index) {
// ...
this.forceRender();
},
removeThePost(post, index) {
// ...
this.forceRender();
},
forceRender() {
this.timestamp = new Date().getTime();
}
}
}
從上面的範例中我們可以知道透過 this.$forceUpdate()
或是修改 :key
都能夠更新畫面,只是更新的範圍不一樣成本當然也不同,所以當我們在使用 v-for
的時候,可以根據可以用的資料去設計使用何種方法可以讓系統更快速穩定。
明天我們要來看為 Vue component 自訂事件,與創造 v-model
的方法。