中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Cookbook組件形式:優化 Vue 組件的運行時性能

發布時間:2020-10-03 05:35:55 來源:腳本之家 閱讀:193 作者:前端小透明 欄目:web開發

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-keysstatus 計算屬性便會自動更新,保證關聯子節點可見狀態的正確。

一切準備就緒,為了度量 Tree 組件的運行性能,我們設定了兩個指標。

初次渲染時間 節點展開 / 折疊時間

在 Tree 組件中添加代碼如下,使用 console.timeconsole.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 停止錄制

Cookbook組件形式:優化 Vue 組件的運行時性能

console.time 輸出的值也會顯示在 Performance 中,幫助我們調試。更多關于 Performance 的內容可以點擊這里查看。

優化運行時性能

條件渲染

我們往下翻閱 Performance 分析結果,發現大部分耗時都在 render 函數上,并且下面還有很多其他函數的調用。

Cookbook組件形式:優化 Vue 組件的運行時性能

在遍歷節點時,對于節點的可見性我們使用的是 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-ifv-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) 來觀察組件的性能損耗,相比優化前有很大的提升。

雙向綁定

在前面的示例中,我們使用 .syncexpanded-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-keysstatus 只在初始化時依賴 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 修改為在 datawatcher 中去獲取 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 工具嘗試查找性能瓶頸。

Cookbook組件形式:優化 Vue 組件的運行時性能

我們發現,在 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 了。

Cookbook組件形式:優化 Vue 組件的運行時性能

性能指標如下,對于初次渲染的提升還是很可觀的。

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 應用性能優化指南。

這樣做的價值

應用性能對于用戶體驗的提升是非常重要的,也往往是容易被忽視的。試想一下,一個在某臺設備運行良好的應用,到了另一臺配置較差的設備上導致用戶瀏覽器崩潰了,這一定是一個不好的體驗。又或者你的應用在常規數據下正常運行,卻在大數據量下需要相當長的等待時間,也許你就因此錯失了一部分用戶。

總結

性能優化是一個長久不衰的話題,沒有一種通用的辦法能夠解決所有的性能問題。性能優化是可以持續不端地進行下去的,但隨著問題的深入,性能瓶頸會越來越不明顯,優化也越困難。

本文的示例具有一定的特殊性,但它為我們指引了性能優化的方法論。

  • 確定衡量運行時性能的指標
  • 確定優化目標,例如實現 1W+ 數據的秒出
  • 使用工具(Chrome Performance)分析性能問題
  • 優先解決問題的大頭(瓶頸)
  • 重復 3 4 步直到實現目標

以上所述是小編給大家介紹的Cookbook組件形式:優化 Vue 組件的運行時性能,希望對大家有所幫助,如果大家有任何疑問歡迎給我留言,小編會及時回復大家的!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

方城县| 南雄市| 肥城市| 朝阳县| 噶尔县| 石柱| 开远市| 南和县| 镇赉县| 斗六市| 鹤壁市| 六盘水市| 昔阳县| 精河县| 余庆县| 庐江县| 兰考县| 靖边县| 长宁区| 万年县| 成武县| 固镇县| 黄山市| 济阳县| 九龙县| 怀集县| 河南省| 晋江市| 南汇区| 于田县| 南川市| 泰和县| 怀柔区| 丰都县| 大宁县| 平安县| 胶南市| 普兰县| 襄垣县| 青神县| 博乐市|