讓Vue專案更絲滑的幾個小技巧

子君發表於2020-07-22
陣陣鍵盤聲,隱隱測試言。產品不穩定,今夜無人還。

在開發Vue的過程中,我們經常會遇到一些這樣那樣的問題,然後要卡好半天,等問題解決了才發現原來一些細節知識點還是沒有掌握好。今天小編就整理了幾個在專案中會用到的一些實戰技巧點,希望可以幫助到正在努力賺錢的你。江湖規矩,先贊後看,豔遇不斷。

本文首發於公眾號【前端有的玩】,關注我,我們一起玩前端,每天都有不一樣的乾貨知識點等著你哦

資料不響應,可能是用法有問題

前幾天有朋友給我發了一段程式碼,然後說Vuebug,他明明寫的沒問題,為啥資料就不響應呢,一定是Vuebug?我感覺他比尤雨溪要牛逼,高攀不起,就沒有理他了。但是確實有時候我們在開發時候會遇到資料不響應的情況,那怎麼辦呢?比如下面這段程式碼:

<template>
  <div>
    <div>
      <span>使用者名稱: {{ userInfo.name }}</span>
      <span>使用者性別: {{ userInfo.sex }}</span>
      <span v-if="userInfo.officialAccount">
        公眾號: {{ userInfo.officialAccount }}
      </span>
    </div>
    <button @click="handleAddOfficialAccount">新增公眾號</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      userInfo: {
        name: '子君',
        sex: '男'
      }
    }
  },
  methods: {
    // 在這裡新增使用者的公眾號
    handleAddOfficialAccount() {
      this.userInfo.officialAccount = '前端有的玩'
    }
  }
}
</script>

在上面的程式碼中,我們希望給使用者資訊裡面新增公眾號屬性,但是通過this.userInfo.officialAccount = '前端有的玩' 新增之後,並沒有生效,這是為什麼呢?

這是因為在Vue內部,資料響應是通過使用Object.definePrototype監聽物件的每一個鍵的getter,setter方法來實現的,但通過這種方法只能監聽到已有屬性,新增的屬性是無法監聽到的,但我就是想監聽,小編你說咋辦吧。下面小編提供了四種方式,如果有更多方式,歡迎下方評論區告訴我。

1. 將本來要新增的屬性提前在data中定義好

比如上面的公眾號,我可以提前在userInfo裡面定義好,這樣就不是新增屬性了,就像下面這樣

data() {
    return {
      userInfo: {
        name: '子君',
        sex: '男',
        // 我先提前定義好
        officialAccount: ''
      }
    }
  }

2. 直接替換掉userInfo

雖然無法給userInfo裡面新增新的屬性,但是因為userInfo已經定義好了,所以我直接修改userInfo的值不就可以了麼,所以也可以像下面這樣寫

this.userInfo = {
  // 將原來的userInfo 通過擴充套件運演算法複製到新的物件裡面
  ...this.userInfo,
  // 新增新屬性
  officialAccount: '前端有的玩'
}

3. 使用Vue.set

其實上面兩種方法都有點取巧的嫌疑,其實對於新增屬性,Vue官方專門提供了一個新的方法Vue.set用來解決新增屬性無法觸發資料響應。

Vue.set 方法定義

/**
* target 要修改的物件
* prpertyName 要新增的屬性名稱
* value 要新增的屬性值
*/
Vue.set( target, propertyName, value )

上面的程式碼使用Vue.set可以修改為

import Vue from 'vue'

// 在這裡新增使用者的公眾號
handleAddOfficialAccount() {
  Vue.set(this.userInfo,'officialAccount', '前端有的玩')
}

但是每次要用到set方法的時候,還要把Vue引入進來,好麻煩,所以為了簡便起見,Vue又將set方法掛載到了Vue的原型鏈上了,即Vue.prototype.$set = Vue.set,所以在Vue元件內部可以直接使用this.$set代替Vue.set

this.$set(this.userInfo,'officialAccount', '前端有的玩')

小編發現有許多同學不知道什麼時候應該用Vue.set,其實只有當你要賦值的屬性還沒有定義的時候需要使用Vue,set,其他時候一般不會需要使用。

4. 使用$forceUpdate

我覺得$forceUpdate的存在,讓許多前端開發者不會再去注意資料雙向繫結的原理,因為不論什麼時候,反正我修改了data之後,呼叫一下$forceUpdate就會讓Vue元件重新渲染,bug是不會存在的。但是實際上這個方法並不建議使用,因為它會引起許多不必要的效能消耗。

針對陣列的特定方式

其實不僅僅是物件,陣列也存在資料修改之後不響應的情況,比如下面這段程式碼

<template>
  <div>
    <ul>
      <li v-for="item in list" :key="item">
        {{ item }}
      </li>
    </ul>
    <button @click="handleChangeName">修改名稱</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      list: ['張三', '李四']
    }
  },
  methods: {
    // 修改使用者名稱稱
    handleChangeName() {
      this.list[0] = '王五'
    }
  }
}
</script>

上面的程式碼希望將張三的名字修改為王五,實際上這個修改並不能生效,這是因為Vue不能檢測到以下變動的陣列:

  1. 當你利用索引直接設定一個項時,例如: this.list[index] = newValue
  2. 修改陣列的length屬性,例如: this.list.length = 0

所以在上例中通過this.list[0] = '王五' 是無法觸發資料響應的,那應該怎麼辦呢?像上面提到的Vue.set$forceUpdate都可以解決這個問題,比如Vue.set可以這樣寫

Vue.set(this.list,0,'王五')
複製程式碼

除了那些方法之外,Vue還針對陣列提供了變異方法

在運算元組的時候,我們一般會用到資料提供的許多方法,比如push,pop,splice等等,在Vue中呼叫陣列上面提供的這些方法修改陣列的值是可以觸發資料響應的,比如上面的程式碼改為以下程式碼即可觸發資料響應

this.list.splice(0,1,'王五')

實際上,如果Vue僅僅依賴gettersetter,是無法做到在陣列呼叫push,pop等方法時候觸發資料響應的,因此Vue實際上是通過劫持這些方法,對這些方法進行包裝變異來實現的。

Vue對陣列的以下方法進行的包裝變異:

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

所以在運算元組的時候,呼叫上面這些方法是可以保證資料可以正常響應,下面是Vue原始碼中包裝陣列方法的程式碼:

var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    // 將 arguments 轉換為陣列
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];
    var result = original.apply(this, args);
    // 這兒的用法同dependArray(value),就是為了取得dep
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    // 如果有新的資料插入,則插入的資料也要進行一個響應式
    if (inserted) { ob.observeArray(inserted); }
   // 通知依賴進行更新
    ob.dep.notify();
    return result
  });

文字格式化,filter更簡單

使用filter 簡化邏輯

我想把時間戳顯示成yyyy-MM-DD HH:mm:ss的格式怎麼辦?是需要在程式碼中先將日期格式化之後,再渲染到模板嗎?就像下面這樣

<template>
  <div>
    {{ dateStr }}
    <ul>
      <li v-for="(item, index) in getList" :key="index">
        {{ item.date }}
      </li>
    </ul>
  </div>
</template>
<script>
import { format } from '@/utils/date'
export default {
  data() {
    return {
      date: Date.now(),
      list: [
        {
          date: Date.now()
        }
      ]
    }
  },
  computed: {
    dateStr() {
      return format(this.date, 'yyyy-MM-DD HH:mm:ss')
    },
    getList() {
      return this.list.map(item => {
        return {
          ...item,
          date: format(item.date, 'yyyy-MM-DD HH:mm:ss')
        }
      })
    }
  }
}
</script>

像上面的寫法,針對每一個日期欄位都需要呼叫format,然後通過計算屬性進行轉換?這時候可以考慮使用Vue提供的filter去簡化

<template>
  <div>
    <!--使用過濾器-->
    {{ dateStr | formatDate }}
    <ul>
      <li v-for="(item, index) in list" :key="index">
        <!--在v-for中使用過濾器-->
        {{ item.date | formatDate }}
      </li>
    </ul>
  </div>
</template>
<script>
import { format } from '@/utils/date'
export default {
  filters: {
    formatDate(value) {
      return format(value, 'yyyy-MM-DD HH:mm:ss')
    }
  },
  data() {
    return {
      date: Date.now(),
      list: [
        {
          date: Date.now()
        }
      ]
    }
  }
}
</script>

通過上面的修改是不是就簡單多了

註冊全域性filter

有些過濾器使用的很頻繁,比如上面提到的日期過濾器,在很多地方都要使用,這時候如果在每一個要用到的元件裡面都去定義一遍,就顯得有些多餘了,這時候就可以考慮Vue.filter註冊全域性過濾器

對於全域性過濾器,一般建議在專案裡面新增filters目錄,然後在filters目錄裡面新增

// filters\index.js

import Vue from 'vue'
import { format } from '@/utils/date'

Vue.filter('formatDate', value => {
  return format(value, 'yyyy-MM-DD HH:mm:ss')
})

然後將filters裡面的檔案引入到main.js裡面,這時候就可以在元件裡面直接用了,比如將前面的程式碼可以修改為

<template>
  <div>
    <!--使用過濾器-->
    {{ dateStr | formatDate }}
    <ul>
      <li v-for="(item, index) in list" :key="index">
        <!--在v-for中使用過濾器-->
        {{ item.date | formatDate }}
      </li>
    </ul>
  </div>
</template>
<script>
export default {
  data() {
    return {
      date: Date.now(),
      list: [
        {
          date: Date.now()
        }
      ]
    }
  }
}
</script>

是不是更簡單了

開發了外掛庫,來安裝一下

在使用一些UI框架的時候,經常需要使用Vue.use來安裝, 比如使用element-ui時候,經常會這樣寫:

import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI,{size: 'small'});

使用了Vue.use之後,element-ui就可以直接在元件裡面使用了,好神奇哦(呸,娘炮)。接下來我們實現一個簡化版的element來看如何去安裝。

瞭解Vue.use的用法

Vue.use是一個全域性的方法,它需要在你呼叫 new Vue() 啟動應用之前完成,Vue.use的引數如下

/**
* plugin: 要安裝的外掛 如 ElementUI
* options: 外掛的配置資訊 如 {size: 'small'}
*/
Vue.use(plugin, options)

模擬element-ui的安裝邏輯

想一下,使用Vue.use(ElementUI,{size: 'small'}) 之後我們可以用到哪些element-ui提供的東西

  1. 可以直接在元件裡面用element-ui的元件,不需要再import
  2. 可以直接使用v-loading指令
  3. 通過this.$loading在元件裡面顯示loading
  4. 其他...
// 這個是一個按鈕元件
import Button from '@/components/button'

// loading 指令
import loadingDirective from '@/components/loading/directive'

// loading 方法
import loadingMethod from '@/components/loading'

export default {
  /**
   * Vue.use 需要外掛提供一個install方法
   * @param {*} Vue Vue
   * @param {*} options 外掛配置資訊
   */
  install(Vue, options) {
    console.log(options)
    // 將元件通過Vue.components 進行註冊
    Vue.components(Button.name, Button)

    // 註冊全域性指令
    Vue.directive('loading', loadingDirective)

    // 將loadingMethod 掛載到 Vue原型鏈上面,方便呼叫
    Vue.prototype.$loading = loadingMethod
  }
}

通過上面的程式碼,已經實現了一個丐版的element-ui外掛,這時候就可以在main.js裡面通過Vue.use進行外掛安裝了。大家可能會有疑問,為什麼我要用這種寫法,不用這種寫法我照樣可以實現功能啊。小編認為這種寫法有兩個優勢

  1. 標準化,通過提供一種統一的開發模式,無論對外掛開發者還是使用者來說,都有一個規範去遵循。
  2. 外掛快取,Vue.use 在安裝外掛的時候,會對外掛進行快取,即一個外掛如果安裝多次,實際上只會在第一次安裝時生效。

外掛的應用場景

  1. 新增全域性方法或者 property。
  2. 新增全域性資源:指令/過濾器/過渡等。
  3. 通過全域性混入來新增一些元件選項。
  4. 新增 Vue 例項方法,通過把它們新增到 Vue.prototype 上實現。
  5. 一個庫,提供自己的 API,同時提供上面提到的一個或多個功能。如element-ui

提高Vue渲染效能,瞭解一下Object.freeze

當一個 Vue 例項被建立時,它將 data 物件中的所有的 property 加入到 Vue 的響應式系統中。當這些 property 的值發生改變時,檢視將會產生“響應”,即匹配更新為新的值。但是這個過程實際上是比較消耗效能的,所以對於一些有大量資料但只是展示的介面來說,並不需要將property加入到響應式系統中,這樣可以提高渲染效能,怎麼做呢,你需要了解一下Object.freeze

Vue官網中,有這樣一段話:這裡唯一的例外是使用 Object.freeze(),這會阻止修改現有的 property,也意味著響應系統無法再_追蹤_變化。這段話的意思是,如果我們的資料使用了Object.freeze,就可以讓資料脫離響應式系統,那麼該如何做呢?

比如下面這個表格,因為只是渲染資料,這時候我們就可以通過Object.freeze來優化效能

<template>
  <el-table :data="tableData" >
    <el-table-column prop="date" label="日期" width="180" />
    <el-table-column prop="name" label="姓名" width="180" />
    <el-table-column prop="address" label="地址" />
  </el-table>
</template>
<script>
export default {
  data() {
    const data = Array(1000)
      .fill(1)
      .map((item, index) => {
        return {
          date: '2020-07-11',
          name: `子君${index}`,
          address: '大西安'
        }
      })
    return {
      // 在這裡我們用了Object.freeze
      tableData: Object.freeze(data)
    }
  }
}
</script>

有的同學可能會有疑問,如果我這個表格的資料是滾動載入的,你這樣寫我不就沒法再給tableData新增資料了嗎?是,確實沒辦法去新增資料了,但還是有辦法解決的,比如像下面這樣

export default {
  data() {
    return {
      tableData: []
    }
  },
  created() {
    setInterval(() => {
      const data = Array(1000)
        .fill(1)
        .map((item, index) => {
          // 雖然不能凍結整個陣列,但是可以凍結每一項資料
          return Object.freeze({
            date: '2020-07-11',
            name: `子君${index}`,
            address: '大西安'
          })
        })
      this.tableData = this.tableData.concat(data)
    }, 2000)
  }
}

合理的使用Object.freeze,是可以節省不少渲染效能,特別對於IE瀏覽器,效果還是很明顯的,趕快去試試吧。

最後如果你現在需要開發移動端專案,可以瞭解一下小編整理的一個開箱即用框架 vue-vant-base,也許可以幫到你哦

結語

不要吹滅你的靈感和你的想象力; 不要成為你的模型的奴隸。 ——文森特・梵高

歡迎大家關注我的公眾號【前端有的玩】,我們一起玩前端~

相關文章