欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Cookbook組件形式:優(yōu)化 Vue 組件的運行時性能

 更新時間:2018年11月25日 11:02:03   作者:前端小透明  
本文仿照Vue Cookbook 組織形式,對優(yōu)化 Vue 組件的運行時性能進行闡述。通過基本的示例代碼給大家講解,需要的朋友參考下

Vue 2.0 在發(fā)布之初,就以其優(yōu)秀的運行時性能著稱,你可以通過這個第三方 benchmark 來對比其他框架的性能。Vue 使用了 Virtual DOM 來進行視圖渲染,當(dāng)數(shù)據(jù)變化時,Vue 會對比前后兩棵組件樹,只將必要的更新同步到視圖上。

Vue 幫我們做了很多,但對于一些復(fù)雜場景,特別是大量的數(shù)據(jù)渲染,我們應(yīng)當(dāng)時刻關(guān)注應(yīng)用的運行時性能。

本文仿照Vue Cookbook 組織形式,對優(yōu)化 Vue 組件的運行時性能進行闡述。

基本的示例

在下面的示例中,我們開發(fā)了一個樹形控件,支持基本的樹形結(jié)構(gòu)展示以及節(jié)點的展開與折疊。

我們定義 Tree 組件的接口如下。 data 綁定了樹形控件的數(shù)據(jù),是若干顆樹組成的數(shù)組, children 表示子節(jié)點。 expanded-keys 綁定了展開的節(jié)點的 key 屬性,使用 sync 修飾符來同步組件內(nèi)部觸發(fā)的節(jié)點展開狀態(tài)的更新。

<template>
 <tree :data="data" expanded-keys.sync="expandedKeys"></tree>
</template>

<script>
export default {
 data() {
 return {
 data: [{
 key: '1',
 label: '節(jié)點 1',
 children: [{
  key: '1-1',
  label: '節(jié)點 1-1'
 }]
 }, {
 key: '2',
 label: '節(jié)點 2'
 }]
 }
 }
};
</script>

Tree 組件的實現(xiàn)如下,這是個稍微復(fù)雜的例子,需要花幾分鐘時間閱讀一下。

<template>
 <ul class="tree">
 <li
 v-for="node in nodes"
 v-show="status[node.key].visible"
 :key="node.key"
 class="tree-node"
 :style="{ 'padding-left': `${node.level * 16}px` }"
 >
 <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 轉(zhuǎn)為一維數(shù)組,方便 v-for 進行遍歷
 // 同時添加 level 和 parent 屬性
 nodes() {
 return this.getNodes(this.data);
 },
 // status 是一個 key 和節(jié)點狀態(tài)的一個 Map 數(shù)據(jù)結(jié)構(gòu)
 status() {
 return this.getStatus(this.nodes);
 },
 },
 methods: {
 // 對 data 進行遞歸,返回一個所有節(jié)點的一維數(shù)組
 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,計算每個節(jié)點的狀態(tài)
 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;
 },
 // 切換節(jié)點的展開狀態(tài)
 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>

展開或折疊節(jié)點時,我們只需更新 expanded-keys , status 計算屬性便會自動更新,保證關(guān)聯(lián)子節(jié)點可見狀態(tài)的正確。

一切準(zhǔn)備就緒,為了度量 Tree 組件的運行性能,我們設(shè)定了兩個指標(biāo)。

初次渲染時間 節(jié)點展開 / 折疊時間

在 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');
 },
};

同時,為了放大可能存在的性能問題,我們編寫了一個方法來生成可控數(shù)量的節(jié)點數(shù)據(jù)。

<template>
 <tree :data="data" :expanded-keys.sync="expandedKeys"></tree>
</template>

<script>
export default {
 data() {
 return {
 // 生成一個有 3 層,每層 10 個共 1000 個節(jié)點的節(jié)點樹
 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: `節(jié)點 ${key}`,
 };
 if (layers > 1) {
  node.children = this.getRandomData(layers - 1, count, node);
 }
 return node;
 });
 },
 },
};
<script>

你可以通過這個CodeSandbox 完整示例來實際觀察下性能損耗。點擊箭頭展開或折疊某個節(jié)點,在 Chrome DevTools 的控制臺(不要使用 CodeSandbox 的控制臺,不準(zhǔn)確)中輸出如下。

first rendering: 406.068115234375ms
expanded change: 231.623779296875ms

在筆者的低功耗筆記本下,初次渲染耗時 400+ms,展開或折疊節(jié)點 200+ms。下面我們來優(yōu)化 Tree 組件的運行性能。

若你的設(shè)備性能強勁,可修改生成的節(jié)點數(shù)量,如 this.getRandomData(4, 10) 生成 10000 個節(jié)點。

使用 Chrome Performance 查找性能瓶頸

Chrome 的 Performance 面板可以錄制一段時間內(nèi)的 js 執(zhí)行細節(jié)及時間。使用 Chrome 開發(fā)者工具分析頁面性能的步驟如下。

打開 Chrome 開發(fā)者工具,切換到 Performance 面板 點擊 Record 開始錄制 刷新頁面或展開某個節(jié)點 點擊 Stop 停止錄制

console.time 輸出的值也會顯示在 Performance 中,幫助我們調(diào)試。更多關(guān)于 Performance 的內(nèi)容可以點擊這里查看。

優(yōu)化運行時性能

條件渲染

我們往下翻閱 Performance 分析結(jié)果,發(fā)現(xiàn)大部分耗時都在 render 函數(shù)上,并且下面還有很多其他函數(shù)的調(diào)用。

在遍歷節(jié)點時,對于節(jié)點的可見性我們使用的是 v-show 指令,不可見的節(jié)點也會渲染出來,然后通過樣式使其不可見。因此嘗試使用 v-if 指令來進行條件渲染。

<li
 v-for="node in nodes"
 v-if="status[node.key].visible"
 :key="node.key"
 class="tree-node"
 :style="{ 'padding-left': `${node.level * 16}px` }"
>
 ...
</li>

v-if 在 render 函數(shù)中表現(xiàn)為一個三目表達式:

visible ? h('li') : this._e() // this._e() 生成一個注釋節(jié)點

v-if 只是減少每次遍歷的時間,并不能減少遍歷的次數(shù)。且Vue.js 風(fēng)格指南中明確指出不要把 v-ifv-for 同時用在同一個元素上,因為這可能會導(dǎo)致不必要的渲染。

我們可以更換為在一個可見節(jié)點的計算屬性上進行遍歷:

<li
 v-for="node in visibleNodes"
 :key="node.key"
 class="tree-node"
 :style="{ 'padding-left': `${node.level * 16}px` }"
>
 ...
</li>

<script>
export {
 // ...
 computed: {
 visibleNodes() {
 return this.nodes.filter(node => this.status[node.key].visible);
 },
 },
 // ...
}
</script>

優(yōu)化后的性能耗時如下:

first rendering: 194.7890625ms
expanded change: 204.01904296875ms

你可以通過改進后的示例 (Demo2) 來觀察組件的性能損耗,相比優(yōu)化前有很大的提升。

雙向綁定

在前面的示例中,我們使用 .syncexpanded-keys 進行了“雙向綁定”,其實際上是 prop 和自定義事件的語法糖。這種方式能很方便地讓 Tree 的父組件同步展開狀態(tài)的更新。

但是,使用 Tree 組件時,不傳 expanded-keys ,會導(dǎo)致節(jié)點無法展開或折疊,即使你不關(guān)心展開或折疊的操作。這里把 expanded-keys 作為外界的副作用了。

<!-- 無法展開 / 折疊節(jié)點 -->
<tree :data="data"></tree>

這里還存在一些性能問題,展開或折疊某一節(jié)點時,觸發(fā)父組件的副作用更新 expanded-keys 。Tree 組件的 status 依賴了 expanded-keys ,會調(diào)用 this.getStatus 方法獲取新的 status 。即使只是單個節(jié)點的狀態(tài)改變,也會導(dǎo)致重新計算所有節(jié)點的狀態(tài)。

我們考慮將 status 作為一個 Tree 組件的內(nèi)部狀態(tài),展開或折疊某個節(jié)點時,直接對 status 進行修改。同時定義默認的展開節(jié)點 default-expanded-keysstatus 只在初始化時依賴 default-expanded-keys 。

export default {
 props: {
 data: Array,
 // 默認展開節(jié)點
 defaultExpandedKeys: {
 type: Array,
 default: () => [],
 },
 },
 data() {
 return {
 status: null, // status 為局部狀態(tài)
 };
 },
 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) {
 // ...
 },
 // 展開或折疊節(jié)點時直接修改 status,并通知父組件
 changeExpanded(key) {
 console.time('expanded change');

 const node = this.nodes.find(n => n.key === key); // 找到該節(jié)點
 const newExpanded = !this.status[key].expanded; // 新的展開狀態(tài)
 
 // 遞歸該節(jié)點的后代節(jié)點,更新 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);

 // 觸發(fā)節(jié)點展開狀態(tài)改變事件
 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 ,節(jié)點也能正常地展開或收起。

<!-- 節(jié)點可以展開或收起 -->
<tree :data="data"></tree>

<!-- 配置默認展開的節(jié)點 -->
<tree
 :data="data"
 :default-expanded-keys="['1', '1-1']"
 @expanded-change="handleExpandedChange"
>
</tree>

優(yōu)化后的性能耗時如下。

first rendering: 91.48193359375ms
expanded change: 20.4287109375ms

你可以通過改進后的示例 (Demo3) 來觀察組件的性能損耗。

凍結(jié)數(shù)據(jù)

到此為止,Tree 組件的性能問題已經(jīng)不是很明顯了。為了進一步擴大性能問題,查找優(yōu)化空間。我們把節(jié)點數(shù)量增加到 10000 個。

// 生成 10000 個節(jié)點
this.getRandomData(4, 1000)

這里,我們故意制造一個可能存在性能問題的改動。雖然這不是必須的,當(dāng)它能幫助我們了解接下來所要介紹的問題。

將計算屬性 nodes 修改為在 datawatcher 中去獲取 nodes 的值。

export default {
 // ...
 watch: {
 data: {
 handler() {
 this.nodes = this.getNodes(this.data);
 this.status = this.getStatus(this.nodes);
 },
 immediate: true,
 },
 // ...
 },
 // ...
};

這種修改對于實現(xiàn)的功能是沒有影響的,那么性能情況如何呢。

first rendering: 490.119140625ms
expanded change: 183.94189453125ms

使用 Performance 工具嘗試查找性能瓶頸。

我們發(fā)現(xiàn),在 getNodes 方法調(diào)用之后,有一段耗時很長的 proxySetter 。這是 Vue 在為 nodes 屬性添加響應(yīng)式,讓 Vue 能夠追蹤依賴的變化。 getStatus 同理。

當(dāng)你把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉(zhuǎn)為 getter/setter。

對象越復(fù)雜,層級越深,這個過程消耗的時間越長。當(dāng)我們存在 1w 個節(jié)點時, proxySetter 的時間就會非常長了。

這里存在一個問題,我們不會對 nodes 某個具體的屬性做修改,而是每當(dāng) data 變化時重新去計算一次。因此,這里為 nodes 添加的響應(yīng)式是無用的。那么怎么把不需要的 proxySetter 去掉呢?一種方法是將 nodes 改回計算屬性,一般情況下計算屬性沒有賦值行為。另一種方法就是凍結(jié)數(shù)據(jù)。

使用 Object.freeze() 來凍結(jié)數(shù)據(jù),這會阻止修改現(xiàn)有的屬性,也意味著響應(yīng)系統(tǒng)無法再追蹤變化。

this.nodes = Object.freeze(this.getNodes(this.data));

查看 Performance 工具, getNodes 方法后已經(jīng)沒有 proxySetter 了。

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

first rendering: 312.22998046875ms
expanded change: 179.59326171875ms

你可以通過改進后的示例 (Demo4) 來觀察組件的性能損耗。

那我們能否用同樣的辦法優(yōu)化 status 的跟蹤呢?答案是否定的,因為我們需要去更新 status 中的屬性值 ( changeExpanded )。因此,這種優(yōu)化只適用于其屬性不會被更新,只會更新整個對象的數(shù)據(jù)。且對于結(jié)構(gòu)越復(fù)雜、層級越深的數(shù)據(jù),優(yōu)化效果越明顯。

替代方案

我們看到,示例中不管是節(jié)點的渲染還是數(shù)據(jù)的計算,都存在大量的循環(huán)或遞歸。對于這種大量數(shù)據(jù)的問題,除了上述提到的針對 Vue 的優(yōu)化外,我們還可以從減少每次循環(huán)的耗時和減少循環(huán)次數(shù)兩個方面進行優(yōu)化。

例如,可以使用字典來優(yōu)化數(shù)據(jù)查找。

// 生成 defaultExpandedKeys 的 Map 對象
const expandedKeysMap = this.defaultExpandedKeys.reduce((map, key) => {
 map[key] = true;
 return map;
}, {});

// 查找時
if (expandedKeysMap[key]) {
 // do something
}

defaultExpandedKeys.includes 的事件復(fù)雜度是 O(n), expandedKeysMap[key] 的時間復(fù)雜度是 O(1)。

更多關(guān)于優(yōu)化 Vue 應(yīng)用性能可以查看Vue 應(yīng)用性能優(yōu)化指南。

這樣做的價值

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

總結(jié)

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

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

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

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

相關(guān)文章

  • Vue.js每天必學(xué)之過渡與動畫

    Vue.js每天必學(xué)之過渡與動畫

    Vue.js每天必學(xué)之過渡與動畫,對Vue.js過渡與動畫進行深入學(xué)習(xí),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-09-09
  • vue如何動態(tài)配置ip與端口

    vue如何動態(tài)配置ip與端口

    這篇文章主要介紹了vue如何動態(tài)配置ip與端口,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-09-09
  • vue3中markRaw示例詳解

    vue3中markRaw示例詳解

    markRaw是將一個對象標(biāo)記為普通對象,而toRaw是將reactive對象變?yōu)槠胀▽ο?在 Vue 3 中,markRaw 是一個用于告訴 Vue 的響應(yīng)性系統(tǒng)不要對某個對象進行轉(zhuǎn)換或追蹤其響應(yīng)性的函數(shù),下面給大家介紹vue3中markRaw詳解,感興趣的朋友一起看看吧
    2024-04-04
  • 利用VUE框架,實現(xiàn)列表分頁功能示例代碼

    利用VUE框架,實現(xiàn)列表分頁功能示例代碼

    本篇文章主要介紹了利用VUE框架,實現(xiàn)列表分頁功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下。
    2017-01-01
  • 淺談sass在vue注意的地方

    淺談sass在vue注意的地方

    下面小編就為大家?guī)硪黄獪\談sass在vue注意的地方。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-08-08
  • vue分頁組件table-pagebar使用實例解析

    vue分頁組件table-pagebar使用實例解析

    這篇文章主要為大家詳細解析了vue分頁組件table-pagebar使用實例,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-08-08
  • 詳解vue項目中調(diào)用百度地圖API使用方法

    詳解vue項目中調(diào)用百度地圖API使用方法

    這篇文章主要介紹了vue項目中調(diào)用百度地圖API使用方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04
  • 手寫Vue源碼之?dāng)?shù)據(jù)劫持示例詳解

    手寫Vue源碼之?dāng)?shù)據(jù)劫持示例詳解

    這篇文章主要給大家介紹了手寫Vue源碼之?dāng)?shù)據(jù)劫持的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-01-01
  • vue 使用插槽分發(fā)內(nèi)容操作示例【單個插槽、具名插槽、作用域插槽】

    vue 使用插槽分發(fā)內(nèi)容操作示例【單個插槽、具名插槽、作用域插槽】

    這篇文章主要介紹了vue 使用插槽分發(fā)內(nèi)容操作,結(jié)合實例形式總結(jié)分析了vue.js使用單個插槽、具名插槽、作用域插槽相關(guān)操作技巧與注意事項,需要的朋友可以參考下
    2020-03-03
  • vue前端el-input輸入限制輸入位數(shù)及輸入規(guī)則

    vue前端el-input輸入限制輸入位數(shù)及輸入規(guī)則

    這篇文章主要給大家介紹了關(guān)于vue前端el-input輸入限制輸入位數(shù)及輸入規(guī)則的相關(guān)資料,文中通過代碼介紹的介紹的非常詳細,對大家學(xué)習(xí)或者使用vue具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-09-09

最新評論