iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 19
0

小弟在開發 Vue 或是 Nuxt 專案的時候,都曾經遇過 v-for 的迭代對象,明明已經被修改了,卻沒有看到畫面的變化,其中有些是 JS 撰寫的一些重點沒注意,有些就真的和 Vue 本身有關。下面就列出幾個曾經犯過的蠢事讓大家笑一笑。

JS 指標觀念

下面是一個包含搜尋功能的 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>

https://gitlab.com/semantic-lab/2020-it-30/raw/master/images/day-18/0.gif

在上面的操作畫面當中我們可以看到,當我們儲存了修改,可是畫面並沒有顯示出修改的資料,我們將 this.posts[this.editIndex] 顯示出來之後,確實是有更新,接著再檢查 this.postsToShow[0],哪尼!! 竟然是原來的資料!? 這個錯誤,其實跟 v-for 毫無關係,這是 JS 指標觀念的問題,從下面第一張圖是搜尋「l」之後,this.posts[2]]this.postsToShow[0] 都是指向同一個 post,然而圖二是修改並儲存之後,只更改了 this.posts,所以當然會出問題。

https://ithelp.ithome.com.tw/upload/images/20190920/20112580m4ikUJKkJh.jpg
https://ithelp.ithome.com.tw/upload/images/20190920/20112580kb8EWmJGgg.jpg

像這樣的錯誤應改為每一個 property 逐一賦值:

        // ...
        this.posts[this.editIndex].title = this.tempPost.title;
        // ...

v-for 渲染時機

下面是一個使用者列表,可以透過點擊標頭完整排序,或是點擊按鈕,自行調整位置。

<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>

https://gitlab.com/semantic-lab/2020-it-30/raw/master/images/day-18/GIF0.gif

當我們點擊標頭進行排序的時候一切都很正常,但是當我們只各別將某個 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();
      },

修改完的結果:
https://gitlab.com/semantic-lab/2020-it-30/raw/master/images/day-18/GIF0-1.gif

Vue 渲染對象

一切看似 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>

然而實際操作的時候卻怪怪的,為甚麼草稿有正常移動到已發文列表,可是草稿列表的項目確有一個被打勾呢?

https://gitlab.com/semantic-lab/2020-it-30/raw/master/images/day-18/GIF1.gif

讓我們再仔細的看一次圖中 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 的方法。


上一篇
Day 18. Vue Component 快速導讀 (2/2)
下一篇
Day 20. 新鮮好吃的手做 v-model
系列文
RRR撞到不負責之 Laravel + Nuxt.js 踩坑全紀錄31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言