VUE Performance - UI Render
Vue2.X版本,通過項目中的實踐中,針對以下幾點來調優渲染性能,
(1)Vue指令綁定
(2)組件劃分
(3)Vuex單向數據流管理與UI響應
NK1 - Retrospect
在進入主題之前,先來回顧上篇文章 《Debounce, Throttle & RequestAnimationFrame》來優化函數觸發頻率過高,也是性能調優的一部分,針對的是JS執行部分,
而本次主題主要圍繞著如何優化HTML的渲染進行展開探討。
NK2 - Agenda
這次的會議主題主要會先分享:通過Vue的指令監聽數據的變化來優化UI的渲染,接著是談談Vue組件劃分和UI渲染的複雜度,最後會分享下在項目中通過使用Vuex的單向數據流管理對UI數據狀態監聽和實時響應UI的變化。
一、 Vue指令
(1)特殊屬性:v-key
(2)條件渲染:v-if & v-show
(3)列表渲染:v-for
在講解指令之前,我們先來簡單了解下Vue的核心之一雙向綁定原理。
NK3 Vue雙向綁定
Vue雙向綁定是指:數據的變化驅動視圖更新,視圖的變化來跟新數據,兩者是相互影響的。
現在很多框架都採用了雙向綁定的原理,但實現方式卻是不一樣的。
例如,
(1)基於觀察者模式: KnockoutJS,BackboneJS
採用發布訂閱模式,通常DOM操作需要引用Jquery庫來進行雙向綁定。
(2)基於數據模型: Ember
將數據與節點元素封裝在一起,數據跟新,遍歷所有綁定的節點,然後跟新節點
(3)基於臟檢查: AngularJS
當觸發UI交互事件和非同步事件(ajax/timeout),觸發臟檢查,即檢查所有的Watchers有沒有變化
(4)基於數據劫持: Vue
而相對於Vue,Vue2.X目前核心的方式是採用Object.defineProperty來實現對屬性的劫持,從而達到數據變動的目的。
我們知道當採用字面量來申明一個對象的時候,即 const object={},當添加或者獲取屬性值的時候,一般不會知道該對象什麼時候被賦值,什麼時候被獲取。
所以Vue在實現雙向綁定上,採用了屬性攔截器Object.defineproperty()來監聽data對象的每個屬性的變化。
Object.defineproperty()主要是為每個屬性添加Getter和Setter方法,但是它不能監聽對象新屬性的添加或刪除,數組索引和長度的變更,所以vue就開放了set()和delete()兩個全局方法來實現對其新屬性的監聽。
Vue3.X新版本開始將採用ES6的Proxy來進行雙向綁定,比較這兩種方式的區別,
Object.defineProperty()
(1)監聽的是屬性的變化,因此我們需要對每個對象的每個屬性進行遍歷,如果屬性值也是對象那麼需要深度遍歷。
(2)無法監控到數組下標的變化,即vm.items[indexOfItem] = newValue這種是無法檢測的。
ES6 Proxy
(1)Proxy可以直接監聽對象而非屬性
(2)可以直接監聽數組下標的變化
NK4 Vue虛擬DOM
你會發現現代Web頁面的大多數邏輯的本質就是不停的修改DOM,但是頻繁跟新DOM,會直接導致整個頁面掉幀,卡頓甚至失去響應,所以基於Vue的雙向綁定,Vue2.X引入了React的虛擬DOM的技術來提升頁面的刷新速度。
在Vue裡面由於依賴追蹤系統的存在,當任意數據變動的時,Vue的每一個組件都精確地知道自己是否需要重繪,所以並不需要手動優化,即針對每個組件是否需要重繪,主要是依賴於VUE 的虛擬DOM來進行優化。
什麼是虛擬DOM呢?
虛擬DOM一開始是FaceBook React項目中所提出技術概念,它是由一個由JS模擬了DOM結構而創建的對象,只存在瀏覽器內存中。它的目的是允許我們能「無所畏懼」地去「刷新」整個頁面。它的功能點主要是確保去渲染UI上真正需要改變的部分,具有高效的Diff演算法和即時的batching(批處理)演算法。
1.Diff演算法
當數據發生變化的時候,Diff演算法能幫助快速對比前後兩個虛擬DOM對象的差異。而它有以下幾點特徵,
(1)最小的粒度:只比較同一層級的節點元素
如果節點類型不同,直接刪掉前面的節點,再創建並插入新的節點,不會再比較這個節點以後的子節點了;如果節點類型相同,則會重新設置該節點的屬性,從而實現節點的更新。
(2)採用復用策略
復用這個詞語在VUE中是個很重要的一個概念,簡單來講就是元素可以被重複使用,很多地方都有用到它,例如對component的劃分,我們通常的設計是讓這個組件能夠被重複使用,而變得只是輸入和輸出的改變,並且作用域是獨立的。而在虛擬DOM優化演算法中,它只是想當數據發生改變的時候,Vue只復用數據變化所在的同一層級且已經被渲染過的元素,而不需要移動DOM元素。
2.Patching(批處理)演算法
把Diff演算法中計算出來的所有差異更新到真實的DOM節點上。
(1)把差異apply到真正的DOM樹上
(2)目的是為了減少DOM節點的Reflow & Repaint
(3)即時狀態,即並不是將所有的修改一次性去跟新DOM的,而是深度遍歷每個差異節點的子節點,然後做出整個節點替換(REPLACE),節點移動(REORDER),節點屬性改變(PROPS)或者文本改變(TEXT)。
NK5 復用策略 - 帶Key的優化
由於VUE會採用復用策略,所以它提供了 "key"這個特殊屬性來最大限度來減少DOM節點的移動,即嘗試修復/再利用相同類型元素的演算法。
例外"key"也可以用於強制替換元素/組件而不是重複使用它->當與if連用的時候。
接下來我們來研究下當在一個數組中添加一個元素時,帶Key與不帶Key時,UI渲染情況是怎樣的呢?
Case:假設我們有五個元素節點A->B->C->D->E,
UI呈現的情況如下,
而HTML部分如下,
(data-v-xx)-> 這是在標記vue文件中css時使用scoped標記產生的,因為要保證各文件中的css不相互影響,給每個component都做了唯一的標記,所以每引入一個component就會出現一個新的data-v-xxx標記.
現在我們的目的想要在位置B和C中間新添加一個新的節點F,如下圖,
接著我們來實踐帶Key與不帶Key的渲染效果
(1)不帶Key的場景
語法:
UI效果:
可以發現B節點之後的所有節點都被重新渲染了一遍。
(2)帶Key的效果
語法:
UI效果:
可以發現UI 僅渲染新增加的F節點元素
結論:
當我們不帶Key優化時,你會發現,先是在B和C節點元素中間插入F節點,然後C節點移動到原先D節點的位置,D移動到E的位置,而E成為了最後一個節點。
不帶Key的復用情況是,
(1)當迭代的是數組的時候,是以數組的值作為唯一標識.
(2)當迭代的是個對象的話,就以對象的key作為標識
當使用key屬性來給每個節點做一個唯一標識,復用情況就不一樣了,會復用已有的Dom節點元素,而Diff演算法還可以正確的識別此新增的節點,並且能快速找到正確的位置區直接插入,
即key的主要作用是為了能高效的跟新虛擬DOM
NK6 條件渲染: v-if & v-show
v-if 和 v-show都是用來控制元素在視圖上顯示的狀態,兩者的使用場景是有區別的。
1.生命周期
(1)v-if 控制著綁定元素或者子組件實例 重新掛載(條件為真)/銷毀(條件為假) 到DOM上,並且包含其元素綁定的事件監聽器,會重新開啟監聽。
(2)v-show 控制CSS的切換,元素永遠掛載在DOM上
2.許可權問題
涉及到許可權相關的UI展示無疑用的是v-if.
3.UI操作
(1)初始化渲染,如果要加快首屏渲染,建議用v-if
(2)頻次選擇,如果是頻繁切換使用,建議使用v-show
4.v-if & v-else管理可復用元素
當跟v-else連用時,其復用DOM的作用域範圍:指令元素的 子元素並且是同級兄弟單節點
例如,當我們使用v-if 和 v-else 來切換UI展示部分,UI和定義html如下圖,
label,input,span就是外層div塊下的子標籤元素,且是同級兄弟單節點,如下圖,
而內部的div屬於包裹這其他元素標籤,屬於複合節點,如下圖。
所以v-if 與 v-else 在條件切換,控制UI顯示的時候,會按標籤元素的順序進行節點的差異比較,如果是單節點,就直接進行高效的復用 DOM 元素,反而複合節點會整個被替換。
UI效果:
當選擇不復用,可以使用key進行控制,用於強制替換元素/組件而不是重複使用它。
即官網提到的:
注意,<label>
元素仍然會被高效地復用,因為它們沒有添加key
屬性。
NK6 列表渲染: v-for
Vue提供指令 v-for來迭代每一行數據,從而實現列表渲染,最後呈現在UI上。
我們的期望是當數據發生變化時,UI能及時響應,所以對於列表的渲染性能優化問題大體都會結合特殊屬性 v-key 來搭配使用,從而達到DOM元素的復用。
對於數組變化的監聽響應,有下面兩種場景,
1.跟新列表的監聽方法
push,pop,shift,unshift,splice,sort,reverse
會直接響應視圖的跟新
2.替換列表的監聽
(1)數組全新賦值,會直接響應視圖跟新
即vm.items = newItems;
(2)VUE2.X利用索引直接替換原有值 =》VUE3.X開始可以忽略這個(ES6 Proxy直接監聽對象)
即vm.items[indexOfItem] = newItem;
computed屬性監聽不了改變,只能採用watch(deep)
解決方案:
- Vue.set(vm.items, indexOfItem, newValue) = vm.$set(vm.items, indexOfItem, newValue)
- vm.items.splice(indexOfItem, 1, newValue)
二、 Vue Component渲染設計
接下來會介紹下組件的分類,組件劃分和渲染的複雜度
NK7 組件分類
組件化開發一開始也是由React提出的核心亮點,主要是為了解決 高耦合,低內聚和無復用的問題。
組件化開發主旨:使用組件都封裝具有獨立功能的UI模塊,以組件的方式去重新思考UI構成,整個界面就是一個大組件,然後將小的組件通過組合或者嵌套的方式構成大的組件,最終完成整體UI的構建。
組件化開發規定組件採用單一原則,即理想狀態下,一個組件只做一件事,只關心自己部分的邏輯,所以組件化開發過程就是不斷優化和拆分界面組件、構造整個組件樹的過程。
對於組件的分類大體可以分為以下四種
1.展示型組件 - Display
純展示型組件只關注數據進,然後直接渲染DOM。
2.接入型組件 - Container
Container主要適用於與數據層service打交道,包含和伺服器端或者數據源打交道的邏輯,一般將數據傳給簡單的展示型組件。
3.交互性組件 - Interaction
Interaction主要是指我們引用的第三方庫,例如element-ui,iview等,主旨強調封裝和高復用。
4.功能型組件 - Funtion
功能型組件作為一種抽象或者擴展存在的機制,在vue的場景下,例如路由組件,首先它不渲染任何內容,它僅是將URL路徑映射到組件樹的結構,它的特點允許可以在父組件中採用路由組件來聲明式渲染其它組件,是一種第三方的擴展機制存在。
NK8 組件(劃分+渲染)的複雜度
引用上一篇的例子:假設我們店鋪有100種水果,我們想觀察15天實際和我們期望時間內銷售完的情況。其功能UI原型如下:
我們來設計該組件的結構,如下
首先針對該功能,我們主要用三個組件來封裝相應的模塊,但項目中遇到最大的困難時TableTitle + TableBody組件的(設計+渲染)問題,因為外界因素(如條件過濾)影響,會跟新數據,從而導致UI重新渲染,如果數據很多的話會出現卡頓。
我們最初的組件設計是以 天數作為一個組件 為粒度,縱向劃分,即如下圖
它的渲染複雜度: O(100 * 15) ->水果的數量 * 觀察的天數,即粒度為每一天的組件包含所有水果的信息,這就導致了如果每一次的數據change,會導致這15個天數的組件一起重新渲染。
而針對優化,我們若轉化另一種角度來設計組件,即以 每種水果作為一個組件 為粒度,橫向劃分,即如下圖,
它的複雜度: O(100) ->水果的數量,即一種水果包含了它所有的觀察天數,這樣當數據變化的時候,它的重新渲染效率由水果的數量所決定。
總結:組件在使用v-for指令進行渲染的時候,除了考慮v-key的設計,還需要衡量縱橫切割組件渲染的複雜度。
三、 Vuex單向數據流管理 & UI響應式
針對Vuex的主題,接下來會從 WHY - WHAT - HOW -OPTIMIZE來分析,
即為什麼項目要採用Vuex,Vuex是什麼,Vuex怎麼使用和實踐過程中如何優化Vuex與UI的渲染的響應。
NK9 VUEX - WHY
先了解組件通信的背景,
當我們採用組件化開發模式時,為了能高效管理數據流動的情況,會約束組件與組件之間通信需要採用單向數據流的模式,即組件Container會通過屬性綁定把數據傳遞給子組件,而如果子組件想要修改傳入的數據必須通過事件回調($emit)和父組件通信。但是這種模式會有個弊端,即子組件可能會包含自己的子子組件,而子子組件又包含子子子組件……
這樣當項目越來越複雜的時候,就會出現以下痛點:
(1)多個組件依賴於同一個狀態
(2)來自不同組件的行為需要變更同一個狀態
即當組件的層級越來越深,會造成父組件可能會與很遠的組件之間共享同份數據,當很遠的組件需要修改數據時,就會造成事件回調需要層層返回,這就是災難,造成代碼很難維護。所以有沒有方案可以解決這個痛點呢?
答案是有的,就是我們首先先拋開 數據層層傳遞,我們迫切希望有個這樣的一個第三方庫,我們所有的組件都能從這個地方進行獲取和修改,讓這個第三方庫能統一維護這份數據的狀態,我們稱為狀態管理庫,Vuex是專門為Vue.js設計第三方,以利用 Vue.js 的細粒度數據(屬性監聽)響應機制來進行高效的狀態更新。
NK10 Vuex - WHAT
什麼是Vuex?
Vuex是一個數據控制中心,集中式存儲管理應用的所有組件的狀態,而狀態的存儲是響應式的,即當狀態跟新的時候,它能高效通知組件跟新。
生命周期:在創建Vue實例應用的時候,注入Store,提供了一種機制將狀態從根組件「注入」到每一個子組件中。
但當我們組件使用computed + mapGetter/mapMutations…等輔助函數綁定使用時,它只在Vue2.X生命周期created的時候才會被初始化。
核心概念
1.State
State是一個對象,存儲該模塊的所有狀態,相當於data屬性,最好在store中申明好所有屬性。否則需要用vue.set(state,屬性,普通值/對象值/數組值) 來讓Vue進行依賴收集。
另外一點,在組件引用的時候,它只提供讀的屬性,不允許組件直接修改狀態。
2.Getter
(1)跟computed計算屬性一樣,getter返回值會根據它的依賴被緩存起來,且只有當它的依賴值發生了改變,才會被重新計算。
復用:當State狀態需要做業務邏輯處理時,而不需要在每個組件都重複申明時,可以在Getter方法中聲明,這樣當提供給多個組件調用時,可以很好維護。
(2)接受的參數: state ,getters ,rootState, rootGetters =》根據根State或者根Getter可以獲取其它模塊的數據狀態
(3)Getter返回一個函數,來實現給getter傳參。 UI可以使用Computer的方式來聲明,然後調用的時候直接傳參就可以。
3.Mutation
4.Action
5.Module
狀態管理庫也可以進行模塊化分裝,使模塊內部的Action,State,Mutation和Getter被帶上「命名」被註冊。
屬性配置 -》 namespaced:true
UI其它小技巧實踐:
利用computed的特性,用get和set來獲取和設值。
NK11 VUEX - HOW
如何在項目中實踐Vuex呢?
- 單向數據流
項目中的一個功能組件,原先是在根組件存儲所有數據的狀態,由於後面需求的增加,需要在其它同級組件維護同一份數據,所以一開始的推翻宏圖設計如下,將基礎數據先存入Store裡面,接著任何組件的改變,都通過提交一個action,在Store裡面直接通過業務邏輯更改數據,然後響應所有組件變化。
2. 採用面向對象思想來管理對象的狀態
但是由於一個項目會有很多不同領域的開發人員參與進來,其採用的開發模式思想也會跟著不一樣,在對此模塊重構過程中,有些同事在基於Vuex的基礎上,在State對象中添加的屬性是採用面向對象的方式封裝的一個獨立Calculator對象。
實現方式:在Store中先申明該屬性為一個對象,該對象封裝自己屬性和方法,在使用方面,先是在組件中申明調用該對象,通過調用該對象的內部方法,來同步更改對象屬性狀態。
而其好處是允許多個組件維護同一份業務邏輯處理方式,需要考慮不同的場景是否需要封裝可復用的對象。
NK12 單向數據流的狀態管理實現
父組件負責渲染Store的數據狀態,然後通過props傳遞數據到子組件中,子組件觸發事件提交更改狀態的action, Store可以在Dispatcher上監聽到Action並做出相應的操作,當數據模型發生變化時,就觸發刷新整個父組件界面。
NK13 Vuex State & UI渲染
State存儲著UI某個模塊的數據狀態,所以當在設計一個屬性為一個對象的時候,需要特別注意注意,當這個對象屬性由於外部條件(數據過濾)的影響,僅發生某條數據狀態變化,而其他數據不變的話,是會響應這個對象屬性的變化,導致引用該對象屬性的組件會被回調, UI會重新渲染。
例如UI功能展示和數據結構如下,
const state = {
shops: {
商鋪A: {
startDate: "2018-11-01",
endDate: "2018-11-30",
loading: false,
diplayMoreFruitsLink: true,
fruits: [30],
},
商鋪B:{
startDate: "2018-11-01",
endDate: "2018-11-30",
loading: false,
diplayMoreFruitsLink: true,
fruits: [100],
},
商鋪C:{……},
}
};
const getters = {
getShopByKey: (state, getters, rootState, rootGetters) => {
return key => {
return state.shops[key];
};
},
};
當模擬將商鋪A和展示的UI來進行綁定
圓圈A- Vuex Action
圓圈S- Vuex State
圓圈G- Vuex Getter
圓圈C- Component,其代碼調用如下
當用戶在對商鋪A做一些Filter的UI操作,例如想看其它時間段的銷售情況,這時UI會提交一個SET_MULTIPLE_TIME_RANGE的Mutation事件,backend會重新載入商鋪A的Fruits的信息。即屬性Fruits信息需要改變 -> 商鋪A數據跟新 -> State對象中的shops 對象屬性跟新,這時Getter監聽到變化後,會通知綁定的組件(商鋪A,商鋪B,商鋪C),然後UI響應變化。
-》 問題:應該只需要渲染商鋪A的信息,而商鋪B和C應該不需要。
在載入Fruits的信息的時候,我們一般會加一個loading的狀態,這時也會觸發提交一個 SET_MULTIPLE_LOADING 的Mutation事件,即當商鋪A的組件處於載入狀態,等載入完了,也會觸發shops對象屬性的跟新,然後Fruits綁定的UI組件(商鋪A,商鋪B,商鋪C)會被觸發重新渲染。
-》 問題:商鋪A,B和C應該不需要渲染。
這只是一個商鋪A的信息跟新情況,試想下該對象屬性shops如果掛載很多個商鋪的話,UI會發生什麼呢?
答案:如果組件綁定Getter的getStopByKey方法的話,都會被觸發,例如像商鋪B,商鋪C的組件會響應變化,這樣會造成JS執行的時間太長,導致UI出現卡頓現象。
優化解決方案:主要優化State的屬性響應設計
const state = {
shops: {
商鋪A: {
startDate: "2018-11-01",
endDate: "2018-11-30",
loading: false,
diplayMoreFruitsLink: true,
},
商鋪B:{
startDate: "2018-11-01",
endDate: "2018-11-30",
loading: false,
diplayMoreFruitsLink: true,
},
商鋪C:{……},
},
fruits_商鋪A: [30],
fruits_商鋪B: [30],
fruits_商鋪C: [30],
};
總結關鍵字:
(1)復用策略 -》 移動演算法優化
(2)組件劃分
(3)Vuex -》變化數據與UI雙向綁定
(4)ES6 proxy對對象的監聽
推薦閱讀: