十個技巧!實現 Vue.js 極致性能優化
Vue是一套用于構建用戶界面的漸進式的JavaScript框架。它具有體積小,更高的運行效率,雙向數據綁定,生態豐富、學習成本低等優點,所以Vue也被廣泛用在移動端跨平臺框架上。接下來,我將為大家梳理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
監聽組件簡化代碼
用法:
-
通過$on(eventName, eventHandler) 偵聽一個事件。
-
通過$once(eventName,eventHandler) 一次性偵聽一個事件。
-
通過$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" tagName="div">
- <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個:
-
將事件處理程序代理到父節點,減少內存占用率。
-
動態生成子節點時能自動 綁定事件處理程序到父節點。
-
不使用事件代理,每個span節點綁定一個click事件,并指向同一個事件處理程序:
- <div>
- <span
- v-for="(item,index) of 100000"
- :key="index"
- @click="handleClick">
- {{item}}
- </span>
- </div>
-
不使用事件代理,每個span節點綁定一個click事件,并指向不同的事件處理程序
- <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) // 改變后的名字-李四
- }