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

  1. 單向數據流

項目中的一個功能組件,原先是在根組件存儲所有數據的狀態,由於後面需求的增加,需要在其它同級組件維護同一份數據,所以一開始的推翻宏圖設計如下,將基礎數據先存入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對對象的監聽


推薦閱讀:
相关文章