您好,登錄后才能下訂單哦!
Vue 2.0 在發布之初,就以其優秀的運行時性能著稱,你可以通過這個第三方 benchmark 來對比其他框架的性能。Vue 使用了 Virtual DOM 來進行視圖渲染,當數據變化時,Vue 會對比前后兩棵組件樹,只將必要的更新同步到視圖上。
Vue 幫我們做了很多,但對于一些復雜場景,特別是大量的數據渲染,我們應當時刻關注應用的運行時性能。
本文仿照Vue Cookbook 組織形式,對優化 Vue 組件的運行時性能進行闡述。
基本的示例
在下面的示例中,我們開發了一個樹形控件,支持基本的樹形結構展示以及節點的展開與折疊。
我們定義 Tree 組件的接口如下。 data
綁定了樹形控件的數據,是若干顆樹組成的數組, children
表示子節點。 expanded-keys
綁定了展開的節點的 key
屬性,使用 sync
修飾符來同步組件內部觸發的節點展開狀態的更新。
<template> <tree :data="data" expanded-keys.sync="expandedKeys"></tree> </template> <script> export default { data() { return { data: [{ key: '1', label: '節點 1', children: [{ key: '1-1', label: '節點 1-1' }] }, { key: '2', label: '節點 2' }] } } }; </script>
Tree 組件的實現如下,這是個稍微復雜的例子,需要花幾分鐘時間閱讀一下。
<template> <ul class="tree"> <li v-for="node in nodes" v-show="status[node.key].visible" :key="node.key" class="tree-node" : > <i v-if="node.children" class="tree-node-arrow" :class="{ expanded: status[node.key].expanded }" @click="changeExpanded(node.key)" > </i> {{ node.label }} </li> </ul> </template> <script> export default { props: { data: Array, expandedKeys: { type: Array, default: () => [], }, }, computed: { // 將 data 轉為一維數組,方便 v-for 進行遍歷 // 同時添加 level 和 parent 屬性 nodes() { return this.getNodes(this.data); }, // status 是一個 key 和節點狀態的一個 Map 數據結構 status() { return this.getStatus(this.nodes); }, }, methods: { // 對 data 進行遞歸,返回一個所有節點的一維數組 getNodes(data, level = 0, parent = null) { let nodes = []; data.forEach((item) => { const node = { level, parent, ...item, }; nodes.push(node); if (item.children) { const children = this.getNodes(item.children, level + 1, node); nodes = [...nodes, ...children]; node.children = children.filter(child => child.level === level + 1); } }); return nodes; }, // 遍歷 nodes,計算每個節點的狀態 getStatus(nodes) { const status = {}; nodes.forEach((node) => { const parentStatus = status[node.parent && node.parent.key] || {}; status[node.key] = { expanded: this.expandedKeys.includes(node.key), visible: node.level === 0 || (parentStatus.expanded && parentStatus.visible), }; }); return status; }, // 切換節點的展開狀態 changeExpanded(key) { const index = this.expandedKeys.indexOf(key); const expandedKeys = [...this.expandedKeys]; if (index >= 0) { expandedKeys.splice(index, 1); } else { expandedKeys.push(key); } this.$emit('update:expandedKeys', expandedKeys); }, }, }; </script>
展開或折疊節點時,我們只需更新 expanded-keys
, status
計算屬性便會自動更新,保證關聯子節點可見狀態的正確。
一切準備就緒,為了度量 Tree 組件的運行性能,我們設定了兩個指標。
初次渲染時間 節點展開 / 折疊時間
在 Tree 組件中添加代碼如下,使用 console.time
和 console.timeEnd
可以輸出某個操作的具體耗時。
export default { // ... methods: { // ... changeExpanded(key) { // ... this.$emit('update:expandedKeys', expandedKeys); console.time('expanded change'); this.$nextTick(() => { console.timeEnd('expanded change'); }); }, }, beforeCreate() { console.time('first rendering'); }, mounted() { console.timeEnd('first rendering'); }, };
同時,為了放大可能存在的性能問題,我們編寫了一個方法來生成可控數量的節點數據。
<template> <tree :data="data" :expanded-keys.sync="expandedKeys"></tree> </template> <script> export default { data() { return { // 生成一個有 3 層,每層 10 個共 1000 個節點的節點樹 data: this.getRandomData(3, 10), expandedKeys: [], }; }, methods: { getRandomData(layers, count, parent) { return Array.from({ length: count }, (v, i) => { const key = (parent ? `${parent.key}-` : '') + (i + 1); const node = { key, label: `節點 ${key}`, }; if (layers > 1) { node.children = this.getRandomData(layers - 1, count, node); } return node; }); }, }, }; <script>
你可以通過這個CodeSandbox 完整示例來實際觀察下性能損耗。點擊箭頭展開或折疊某個節點,在 Chrome DevTools 的控制臺(不要使用 CodeSandbox 的控制臺,不準確)中輸出如下。
first rendering: 406.068115234375ms expanded change: 231.623779296875ms
在筆者的低功耗筆記本下,初次渲染耗時 400+ms,展開或折疊節點 200+ms。下面我們來優化 Tree 組件的運行性能。
若你的設備性能強勁,可修改生成的節點數量,如 this.getRandomData(4, 10)
生成 10000 個節點。
使用 Chrome Performance 查找性能瓶頸
Chrome 的 Performance 面板可以錄制一段時間內的 js 執行細節及時間。使用 Chrome 開發者工具分析頁面性能的步驟如下。
打開 Chrome 開發者工具,切換到 Performance 面板 點擊 Record 開始錄制 刷新頁面或展開某個節點 點擊 Stop 停止錄制
console.time
輸出的值也會顯示在 Performance 中,幫助我們調試。更多關于 Performance 的內容可以點擊這里查看。
優化運行時性能
條件渲染
我們往下翻閱 Performance 分析結果,發現大部分耗時都在 render 函數上,并且下面還有很多其他函數的調用。
在遍歷節點時,對于節點的可見性我們使用的是 v-show
指令,不可見的節點也會渲染出來,然后通過樣式使其不可見。因此嘗試使用 v-if
指令來進行條件渲染。
<li v-for="node in nodes" v-if="status[node.key].visible" :key="node.key" class="tree-node" : > ... </li>
v-if
在 render 函數中表現為一個三目表達式:
visible ? h('li') : this._e() // this._e() 生成一個注釋節點
即 v-if
只是減少每次遍歷的時間,并不能減少遍歷的次數。且Vue.js 風格指南中明確指出不要把 v-if
和 v-for
同時用在同一個元素上,因為這可能會導致不必要的渲染。
我們可以更換為在一個可見節點的計算屬性上進行遍歷:
<li v-for="node in visibleNodes" :key="node.key" class="tree-node" : > ... </li> <script> export { // ... computed: { visibleNodes() { return this.nodes.filter(node => this.status[node.key].visible); }, }, // ... } </script>
優化后的性能耗時如下:
first rendering: 194.7890625ms expanded change: 204.01904296875ms
你可以通過改進后的示例 (Demo2) 來觀察組件的性能損耗,相比優化前有很大的提升。
雙向綁定
在前面的示例中,我們使用 .sync
對 expanded-keys
進行了“雙向綁定”,其實際上是 prop 和自定義事件的語法糖。這種方式能很方便地讓 Tree 的父組件同步展開狀態的更新。
但是,使用 Tree 組件時,不傳 expanded-keys
,會導致節點無法展開或折疊,即使你不關心展開或折疊的操作。這里把 expanded-keys
作為外界的副作用了。
<!-- 無法展開 / 折疊節點 --> <tree :data="data"></tree>
這里還存在一些性能問題,展開或折疊某一節點時,觸發父組件的副作用更新 expanded-keys
。Tree 組件的 status
依賴了 expanded-keys
,會調用 this.getStatus
方法獲取新的 status
。即使只是單個節點的狀態改變,也會導致重新計算所有節點的狀態。
我們考慮將 status
作為一個 Tree 組件的內部狀態,展開或折疊某個節點時,直接對 status
進行修改。同時定義默認的展開節點 default-expanded-keys
。 status
只在初始化時依賴 default-expanded-keys
。
export default { props: { data: Array, // 默認展開節點 defaultExpandedKeys: { type: Array, default: () => [], }, }, data() { return { status: null, // status 為局部狀態 }; }, computed: { nodes() { return this.getNodes(this.data); }, }, watch: { nodes: { // nodes 改變時重新計算 status handler() { this.status = this.getStatus(this.nodes); }, // 初始化 status immediate: true, }, // defaultExpandedKeys 改變時重新計算 status defaultExpandedKeys() { this.status = this.getStatus(this.nodes); }, }, methods: { getNodes(data, level = 0, parent = null) { // ... }, getStatus(nodes) { // ... }, // 展開或折疊節點時直接修改 status,并通知父組件 changeExpanded(key) { console.time('expanded change'); const node = this.nodes.find(n => n.key === key); // 找到該節點 const newExpanded = !this.status[key].expanded; // 新的展開狀態 // 遞歸該節點的后代節點,更新 status const updateVisible = (n, visible) => { n.children.forEach((child) => { this.status[child.key].visible = visible && this.status[n.key].expanded; if (child.children) updateVisible(child, visible); }); }; this.status[key].expanded = newExpanded; updateVisible(node, newExpanded); // 觸發節點展開狀態改變事件 this.$emit('expanded-change', node, newExpanded, this.nodes.filter(n => this.status[n.key].expanded)); this.$nextTick(() => { console.timeEnd('expanded change'); }); }, }, beforeCreate() { console.time('first rendering'); }, mounted() { console.timeEnd('first rendering'); }, };
使用 Tree 組件時,即使不傳 default-expanded-keys
,節點也能正常地展開或收起。
<!-- 節點可以展開或收起 --> <tree :data="data"></tree> <!-- 配置默認展開的節點 --> <tree :data="data" :default-expanded-keys="['1', '1-1']" @expanded-change="handleExpandedChange" > </tree>
優化后的性能耗時如下。
first rendering: 91.48193359375ms expanded change: 20.4287109375ms
你可以通過改進后的示例 (Demo3) 來觀察組件的性能損耗。
凍結數據
到此為止,Tree 組件的性能問題已經不是很明顯了。為了進一步擴大性能問題,查找優化空間。我們把節點數量增加到 10000 個。
// 生成 10000 個節點 this.getRandomData(4, 1000)
這里,我們故意制造一個可能存在性能問題的改動。雖然這不是必須的,當它能幫助我們了解接下來所要介紹的問題。
將計算屬性 nodes
修改為在 data
的 watcher
中去獲取 nodes
的值。
export default { // ... watch: { data: { handler() { this.nodes = this.getNodes(this.data); this.status = this.getStatus(this.nodes); }, immediate: true, }, // ... }, // ... };
這種修改對于實現的功能是沒有影響的,那么性能情況如何呢。
first rendering: 490.119140625ms expanded change: 183.94189453125ms
使用 Performance 工具嘗試查找性能瓶頸。
我們發現,在 getNodes
方法調用之后,有一段耗時很長的 proxySetter
。這是 Vue 在為 nodes
屬性添加響應式,讓 Vue 能夠追蹤依賴的變化。 getStatus
同理。
當你把一個普通的 JavaScript 對象傳給 Vue 實例的 data
選項,Vue 將遍歷此對象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。
對象越復雜,層級越深,這個過程消耗的時間越長。當我們存在 1w 個節點時, proxySetter
的時間就會非常長了。
這里存在一個問題,我們不會對 nodes
某個具體的屬性做修改,而是每當 data
變化時重新去計算一次。因此,這里為 nodes
添加的響應式是無用的。那么怎么把不需要的 proxySetter
去掉呢?一種方法是將 nodes
改回計算屬性,一般情況下計算屬性沒有賦值行為。另一種方法就是凍結數據。
使用 Object.freeze()
來凍結數據,這會阻止修改現有的屬性,也意味著響應系統無法再追蹤變化。
this.nodes = Object.freeze(this.getNodes(this.data));
查看 Performance 工具, getNodes
方法后已經沒有 proxySetter
了。
性能指標如下,對于初次渲染的提升還是很可觀的。
first rendering: 312.22998046875ms expanded change: 179.59326171875ms
你可以通過改進后的示例 (Demo4) 來觀察組件的性能損耗。
那我們能否用同樣的辦法優化 status
的跟蹤呢?答案是否定的,因為我們需要去更新 status
中的屬性值 ( changeExpanded
)。因此,這種優化只適用于其屬性不會被更新,只會更新整個對象的數據。且對于結構越復雜、層級越深的數據,優化效果越明顯。
替代方案
我們看到,示例中不管是節點的渲染還是數據的計算,都存在大量的循環或遞歸。對于這種大量數據的問題,除了上述提到的針對 Vue 的優化外,我們還可以從減少每次循環的耗時和減少循環次數兩個方面進行優化。
例如,可以使用字典來優化數據查找。
// 生成 defaultExpandedKeys 的 Map 對象 const expandedKeysMap = this.defaultExpandedKeys.reduce((map, key) => { map[key] = true; return map; }, {}); // 查找時 if (expandedKeysMap[key]) { // do something }
defaultExpandedKeys.includes
的事件復雜度是 O(n), expandedKeysMap[key]
的時間復雜度是 O(1)。
更多關于優化 Vue 應用性能可以查看Vue 應用性能優化指南。
這樣做的價值
應用性能對于用戶體驗的提升是非常重要的,也往往是容易被忽視的。試想一下,一個在某臺設備運行良好的應用,到了另一臺配置較差的設備上導致用戶瀏覽器崩潰了,這一定是一個不好的體驗。又或者你的應用在常規數據下正常運行,卻在大數據量下需要相當長的等待時間,也許你就因此錯失了一部分用戶。
總結
性能優化是一個長久不衰的話題,沒有一種通用的辦法能夠解決所有的性能問題。性能優化是可以持續不端地進行下去的,但隨著問題的深入,性能瓶頸會越來越不明顯,優化也越困難。
本文的示例具有一定的特殊性,但它為我們指引了性能優化的方法論。
以上所述是小編給大家介紹的Cookbook組件形式:優化 Vue 組件的運行時性能,希望對大家有所幫助,如果大家有任何疑問歡迎給我留言,小編會及時回復大家的!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。