10 個技巧!實現 Vue-js 極致性能優化

Vue 框架通過數據雙向綁定和虛擬 DOM 技術,幫我們處理了前端開發中最髒最累的 DOM 操作部分,我們不再需要去考慮如何操作 DOM 以及如何最高效地操作 DOM,但是我們仍然需要去關注 Vue 在跨平臺項目性能方面的優化,使項目具有更高效的性能、更好的用戶體驗。

一、v-for 遍歷必須爲 item 添加 key,且避免同時使用 v-if

在列表數據進行遍歷渲染時,需要爲每一項 item 設置唯一 key 值,方便 Vue.js 內部機制精準找到該條列表數據。當 state 更新時,新的狀態值和舊的狀態值對比,較快地定位到 diff。

我們在使用的使用經常會使用 index(即數組的下標) 來作爲 key,但其實這是不推薦的一種使用方法。

舉個例子:

var list = [
    {
        id: 1,
        name: 'test1',
    },
    {
        id: 2,
        name: 'test2',
    },
    {
        id: 3,
        name: 'test3',
    },
]
<div v-for="(item, index) in list" :key="index" >{{item.name}}</div>

在最後一條數據後再加一條數據:

var list = [
    {
        id: 1,
        name: 'test1',
    },
    {
        id: 2,
        name: 'test2',
    },
    {
        id: 3,
        name: 'test3',
    },
    {
        id: 4,
        name: '我是在最後添加的一條數據',
    },
]

此時前三條數據直接複用之前的,新渲染最後一條數據,此時用 index 作爲 key,沒有任何問題。

在中間插入一條數據:

var list = [
    {
        id: 1,
        name: 'test1',
    },
    {
        id: 4,
        name: '我是插隊的那條數據',
    },
    {
        id: 2,
        name: 'test2',
    },
    {
        id: 3,
        name: 'test3',
    },
]

此時更新渲染數據,通過 index 定義的 key 去進行前後數據的對比,發現:

之前的數據                         之後的數據
key: 0  index: 0 name: test1     key: 0  index: 0 name: test1
key: 1  index: 1 name: test2     key: 1  index: 1 name: 我是插隊的那條數據
key: 2  index: 2 name: test3     key: 2  index: 2 name: test2
                                 key: 3  index: 3 name: test3

通過上面清晰的對比,發現除了第一個數據可以複用之前的之外,另外三條數據都需要重新渲染。

是不是很驚奇,我明明只是插入了一條數據,怎麼三條數據都要重新渲染?而我想要的只是新增的那一條數據新渲染出來就行了。

最好的辦法是使用數組中不會變化的那一項作爲 key 值,對應到項目中, 即每條數據都有一個唯一的 id,來標識這條數據的唯一性;使用 id 作爲 key 值,我們再來對比一下向中間插入一條數據,此時會怎麼去渲染。

之前的數據                               之後的數據
key: 1  id: 1 index: 0 name: test1     key: 1  id: 1 index: 0  name: test1
key: 2  id: 2 index: 1 name: test2     key: 4  id: 4 index: 1  name: 我是插隊的那條數據
key: 3  id: 3 index: 2 name: test3     key: 2  id: 2 index: 2  name: test2
                                       key: 3  id: 3 index: 3  name: test3

現在對比發現只有一條數據變化了,就是 id 爲 4 的那條數據,因此只要新渲染這一條數據就可以了,其他都是就複用之前的。

總結:所以一句話,key 的作用主要是爲了高效的更新虛擬 DOM。另外 Vue 中在使用相同標籤名元素的過渡切換時,也會使用到 key 屬性,其目的也是爲了讓 Vue 可以區分它們,否則 Vue 只會替換其內部屬性而不會觸發過渡效果。

v-for 遍歷避免同時使用 v-if,v-for 比 v-if 優先級高,如果每一次都需要遍歷整個數組,將會影響速度,尤其是當之需要渲染很小一部分的時候,必要情況下應該替換成 computed 屬性。

二、長列表性能優化

Vue 會通過 Object.defineProperty 對數據進行劫持,來實現視圖響應數據的變化,然而有些時候我們的組件就是純粹的數據展示,不會有任何改變,我們就不需要 Vue 來劫持我們的數據,在大量數據展示的情況下,這能夠很明顯的減少組件初始化的時間,那如何禁止 Vue 劫持我們的數據呢?可以通過 Object.freeze 方法來凍結一個對象,一旦被凍結的對象就再也不能被修改了。

export default {
  data: () => ({
    users: {}
  }),
  async created() {
    const users = await axios.get("/api/users");
    this.users = Object.freeze(users);
  }
};

三、Vue 組件中的 data 是函數而不是對象

export default {
  data() {
    // data是一個函數,data: function() {}的簡寫
    return {
      // 頁面要初始化的數據
      name: 'bartonwang',
    };
  },
};

而非如下所示:

export default {
  data: {
    // data是一個對象
    name: 'bartonwang',
  },
};

當一個組件被定義,data 必須聲明爲返回一個初始數據對象的函數,因爲組件可能被用來創建多個實例,複用在多個頁面。

如果 data 是一個純碎的對象,則所有的實例將共享引用同一份 data 數據對象,無論在哪個組件實例中修改 data,都會影響到所有的組件實例。

如果 data 是函數,每次創建一個新實例後,調用 data 函數,從而返回初始數據的一個全新副本數據對象。

這樣每複用一次組件,會返回一份新的 data 數據,類似於給每個組件實例創建一個私有的數據空間,讓各個組件的實例各自獨立,互不影響,保持低耦合。

四、Vue 鉤子函數之鉤子事件 hookEvent,監聽組件簡化代碼

用法:

  1. 通過 $on(eventName, eventHandler) 偵聽一個事件。

  2. 通過 $once(eventName,eventHandler) 一次性偵聽一個事件。

  3. 通過 $off(eventName, eventHandler) 停止偵聽一個事件。

通常實現一個定時器的調用與銷燬我可能會以以下方式實現:

export default{
  data(){
    timer:null  // 需要創建實例
  },
  mounted(){
      this.timer = setInterval(()=>{
      //具體執行內容
      console.log('1');
    },1000);
  }
  beforeDestory(){
    clearInterval(this.timer);
    this.timer = null;
  }
}

這種方法存在的問題是:

vue 實例中需要有這個定時器的實例,感覺有點多餘。創建的定時器代碼和銷燬定時器的代碼沒有放在一起,不容易維護,通常很容易忘記去清理這個定時器。

使用 $on(‘hook:’) 監聽 beforeDestory 生命週期可以避免該問題,並且因爲只需要監聽一次,所以使用 $once 進行註冊監聽。

export default{
  methods:{
    fn(){
      const timer = setInterval(()=>{
        console.log('1');
      },1000);
      this.$once('hook:beforeDestory',()=>{ // 監聽一次即可
        clearInterval(timer);
        timer = null;
      })
    }
  }
}

五、組件懶加載

在單頁應用中,如果沒有應用懶加載,運用 webpack 打包後的文件將會異常地大,造成進入首頁時需要加載的內容過多,延時過長,不利於用戶體驗,而運用懶加載則可以將頁面進行劃分,需要的時候加載頁面,可以有效的分擔首頁所承擔的加載壓力,減少首頁加載用時。

Vue.js 2.0 組件級懶加載方案:

安裝:

npm install@xunlei/vue-lazy-component

在組件中實現局部註冊組件:

import { component as VueLazyComponent } from '@xunlei/vue-lazy-component'
export default {
  components: {
    'vue-lazy-component': VueLazyComponent
  }
}

需要懶加載的組件將其包裹在 vue-lazy-component 中,slot 值爲 skeleton 指的是在懶加載過程中顯示的加載狀態組件。

<vue-lazy-component :timeout="5000" tag>
      <child1 slot="skeleton" />
      <child2 />
      <child3 />
      <child4 />
      <child5 />
</vue-lazy-component>

六、非響應式數據

初始化時,Vue 會對 data 做 getter、setter 改造。在 Vue 的文檔中介紹數據綁定和響應時,特意標註了對於經過 Object.freeze() 方法的對象無法進行更新響應。

性能提升對比

在基於 Vue 的一個 big table benchmark 裏,可以看到在渲染一個一個 1000x10 的表格的時候,開啓 Object.freeze() 前後重新渲染的對比。

開啓優化之前

開啓優化之後

在這個例子裏,使用了 Object.freeze() 比不使用快了 4 倍。

爲什麼 Object.freeze() 的性能會更好,不使用 Object.freeze() 的 CPU 開銷?

使用 Object.freeze() 的 CPU 開銷:

對比可以看出,使用了 Object.freeze() 之後,減少了 observer 的開銷。

七、不要將所有的數據都放到 data 中

data 中的數據都會增加 getter 和 setter,又會收集 watcher,這樣還佔內存。不需要響應式的數據我們可以定義在實例上。

八、v-for 元素綁定事件代理

事件代理作用主要是 2 個:

  1. 將事件處理程序代理到父節點,減少內存佔用率。

  2. 動態生成子節點時能自動綁定事件處理程序到父節點。

<div>
      <span 
        v-for="(item,index) of 100000" 
        :key="index" 
        @click="handleClick">
        {{item}}
      </span>
 </div>
<div>
      <span 
        v-for="(item,index) of 100000" 
        :key="index" 
        @click="function () {}">
        {{item}}
      </span>
  </div>
<div  @click="handleClick">
      <span 
        v-for="(item,index) of 100000"  
        :key="index">
        {{item}}
      </span>
 </div>

可以看到使用事件代理無論是監聽器數量和內存佔用率都比前兩者要少,同時對比 3 個圖中監聽器的數量並沒有發現 Vue 會自動做事件代理,但是一般給 v-for 綁定事件時,都會讓節點指向同一個事件處理程序(第二種情況可以運行,但是 eslint 會警告),一定程度上比每生成一個節點都綁定一個不同的事件處理程序性能好,但是監聽器的數量仍不會變,所以使用事件代理會更好一點。

代碼使用:

<ul @click="meths">
      <li v-for="(item,key) in 10" :key="key" :data-index="key">{{item}}</li>
 </ul>
meths(e) {
      if (e.target.nodeName.toLowerCase() === 'li') {
        console.log(e.target.innerHTML)
        console.log(e.target.dataset)
      }
}

九、函數式組件

函數式組件是無狀態,它無法實例化,沒有任何的生命週期和方法。創建函數式組件也很簡單,只需要在模板添加 functional 聲明即可。一般適合只依賴於外部數據的變化而變化的組件,因其輕量,渲染性能也會有所提高。

組件需要的一切都是通過 context 參數傳遞。它是一個上下文對象,具體屬性查看文檔。這裏 props 是一個包含所有綁定屬性的對象。

函數式組件

十、函數式組件 provide 和 inject 組件通信

痛點:常用的父子組件通信方式都是父組件綁定要傳遞給子組件的數據,子組件通過 props 屬性接收,一旦組件層級變多時,採用這種方式一級一級傳遞值非常麻煩,而且代碼可讀性不高,不便後期維護。

Vue 提供了 provide 和 inject 幫助我們解決多層次嵌套嵌套通信問題。在 provide 中指定要傳遞給子孫組件的數據,子孫組件通過 inject 注入祖父組件傳遞過來的數據,可以輕鬆實現跨級訪問父組件的數據。

provide:是一個對象,或者是一個返回對象的函數。裏面呢就包含要給子孫後代的東西,也就是屬性和屬性值。注意:子孫層的 provide 會掩蓋祖父層 provide 中相同 key 的屬性值

inject:一個字符串數組,或者是一個對象。屬性值可以是一個對象,包含 from 和 default 默認值,from 是在可用的注入內容中搜索用的 key (字符串或 Symbol),意思就是祖父多層 provide 提供了很多數據,from 屬性指定取哪一個 key;default 指定默認值。

從上面這個例子可以看出,只要在父組件中調用了,那麼在這個父組件生效的生命週期內,所有的子組件都可以調用 inject 來注入父組件中的值。

在使用場景中,肯定是希望父組件的數據一旦發生改變,子孫組件獲取到的也是父組件更新後的數據。那麼,怎麼實現父組件與子孫組件所綁定的數據動態響應呢?

-------------------parent.vue----------------------
provide(){
    return {
   // keyName: {name:this.name}, // value 是對象才能實現響應式,也就是引用類型
      keyName: this.changeValue // 通過函數的方式也可以[注意,這裏是把函數作爲value,而不是this.changeValue()]
   // keyName: 'test' value 如果是基本類型,就無法實現響應式
    }
  },
data(){
  return {
      name:'張三'
  }
  },
  methods: {
  changeValue(){
      this.name = '改變後的名字-李四'
   }
  }  
  -------------grandson.vue-----------------
  inject:['keyName']
  create(){
     console.log(this.keyName) // 改變後的名字-李四
}

作者簡介

王雄

騰訊客戶端開發工程師

騰訊客戶端開發工程師,目前在 IEG 增值服務部從事掌上道聚城 app 開發工作,有豐富的跨平臺 weex,react-native,flutter 開發經驗。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/C9bRBoC-edEx2gT28Q-i4A