<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          萬字長文!總結(jié) Vue 性能優(yōu)化方式及原理(收藏)

          共 18290字,需瀏覽 37分鐘

           ·

          2021-06-30 15:13

          前言

          我們在使用 Vue 或其他框架的日常開發(fā)中,或多或少的都會遇到一些性能問題,盡管 Vue 內(nèi)部已經(jīng)幫助我們做了許多優(yōu)化,但是還是有些問題是需要我們主動去避免的。我在我的日常開中,以及網(wǎng)上各種大佬的文章中總結(jié)了一些容易產(chǎn)生性能問題的場景以及針對這些問題優(yōu)化的技巧,這篇文章就來探討下,希望對你有所幫助。

          使用v-slot:slotName,而不是slot="slotName"

          v-slot是 2.6 新增的語法,具體可查看:Vue2.6,2.6 發(fā)布已經(jīng)是快兩年前的事情了,但是現(xiàn)在仍然有不少人仍然在使用slot="slotName"這個語法。雖然這兩個語法都能達(dá)到相同的效果,但是內(nèi)部的邏輯確實不一樣的,下面來看下這兩種方式有什么不同之處。

          我們先來看下這兩種語法分別會被編譯成什么:

          使用新的寫法,對于父組件中的以下模板:

          <child>
          ??<template?v-slot:name>{{name}}</template>
          </child>
          復(fù)制代碼

          會被編譯成:

          function?render()?{
          ??with?(this)?{
          ????return?_c('child',?{
          ??????scopedSlots:?_u([
          ????????{
          ??????????key:?'name',
          ??????????fn:?function?()?{
          ????????????return?[_v(_s(name))]
          ??????????},
          ??????????proxy:?true
          ????????}
          ??????])
          ????})
          ??}
          }
          復(fù)制代碼

          使用舊的寫法,對于以下模板:

          <child>
          ??<template?slot="name">{{name}}</template>
          </child>
          復(fù)制代碼

          會被編譯成:

          function?render()?{
          ??with?(this)?{
          ????return?_c(
          ??????'child',
          ??????[
          ????????_c(
          ??????????'template',
          ??????????{
          ????????????slot:?'name'
          ??????????},
          ??????????[_v(_s(name))]
          ????????)
          ??????],
          ????)
          ??}
          }
          復(fù)制代碼

          通過編譯后的代碼可以發(fā)現(xiàn),舊的寫法是將插槽內(nèi)容作為 children 渲染的,會在父組件的渲染函數(shù)中創(chuàng)建,插槽內(nèi)容的依賴會被父組件收集(name 的 dep 收集到父組件的渲染 watcher),而新的寫法將插槽內(nèi)容放在了 scopedSlots 中,會在子組件的渲染函數(shù)中調(diào)用,插槽內(nèi)容的依賴會被子組件收集(name 的 dep 收集到子組件的渲染 watcher),最終導(dǎo)致的結(jié)果就是:當(dāng)我們修改 name 這個屬性時,舊的寫法是調(diào)用父組件的更新(調(diào)用父組件的渲染 watcher),然后在父組件更新過程中調(diào)用子組件更新(prePatch => updateChildComponent),而新的寫法則是直接調(diào)用子組件的更新(調(diào)用子組件的渲染 watcher)。

          這樣一來,舊的寫法在更新時就多了一個父組件更新的過程,而新的寫法由于直接更新子組件,就會更加高效,性能更好,所以推薦始終使用v-slot:slotName語法。

          使用計算屬性

          這一點已經(jīng)被提及很多次了,計算屬性最大的一個特點就是它是可以被緩存的,這個緩存指的是只要它的依賴的不發(fā)生改變,它就不會被重新求值,再次訪問時會直接拿到緩存的值,在做一些復(fù)雜的計算時,可以極大提升性能。可以看以下代碼:

          <template>
          ??<div>{{superCount}}</div>
          </template>
          <script>
          ??export?default?{
          ????data()?{
          ??????return?{
          ????????count:?1
          ??????}
          ????},
          ????computed:?{
          ??????superCount()?{
          ????????let?superCount?=?this.count
          ????????//?假設(shè)這里有個復(fù)雜的計算
          ????????for?(let?i?=?0;?i?<?10000;?i++)?{
          ??????????superCount++
          ????????}
          ????????return?superCount
          ??????}
          ????}
          ??}
          </script>
          復(fù)制代碼

          這個例子中,在 created、mounted 以及模板中都訪問了 superCount 屬性,這三次訪問中,實際上只有第一次即created時才會對 superCount 求值,由于 count 屬性并未改變,其余兩次都是直接返回緩存的 value,對于計算屬性更加詳細(xì)的介紹可以看我之前寫的文章:Vue computed 是如何實現(xiàn)的?。

          使用函數(shù)式組件

          對于某些組件,如果我們只是用來顯示一些數(shù)據(jù),不需要管理狀態(tài),監(jiān)聽數(shù)據(jù)等,那么就可以用函數(shù)式組件。函數(shù)式組件是無狀態(tài)的,無實例的,在初始化時不需要初始化狀態(tài),不需要創(chuàng)建實例,也不需要去處理生命周期等,相比有狀態(tài)組件,會更加輕量,同時性能也更好。具體的函數(shù)式組件使用方式可參考官方文檔:函數(shù)式組件

          我們可以寫一個簡單的 demo 來驗證下這個優(yōu)化:

          //?UserProfile.vue
          <template>
          ??<div?class="user-profile">{{?name?}}</div>
          </template>

          <script>
          ??export?default?{
          ????props:?['name'],
          ????data()?{
          ??????return?{}
          ????},
          ????methods:?{}
          ??}
          </script>
          <style?scoped></style>

          //?App.vue
          <template>
          ??<div?id="app">
          ????<UserProfile?v-for="item?in?list"?:key="item"?:?/>
          ??</div>
          </template>

          <script>
          ??import?UserProfile?from?'./components/UserProfile'

          ??export?default?{
          ????name:?'App',
          ????components:?{?UserProfile?},
          ????data()?{
          ??????return?{
          ????????list:?Array(500)
          ??????????.fill(null)
          ??????????.map((_,?idx)?=>?'Test'?+?idx)
          ??????}
          ????},
          ????beforeMount()?{
          ??????this.start?=?Date.now()
          ????},
          ????mounted()?{
          ??????console.log('用時:',?Date.now()?-?this.start)
          ????}
          ??}
          </script>

          <style></style>
          復(fù)制代碼

          UserProfile 這個組件只渲染了 props 的 name,然后在 App.vue 中調(diào)用 500 次,統(tǒng)計從 beforeMount 到 mounted 的耗時,即為 500 個子組件(UserProfile)初始化的耗時。

          經(jīng)過我多次嘗試后,發(fā)現(xiàn)耗時一直在 30ms 左右,那么現(xiàn)在我們再把改成 UserProfile 改成函數(shù)式組件:

          <template?functional>
          ??<div?class="user-profile">{{?props.name?}}</div>
          </template>
          復(fù)制代碼

          此時再經(jīng)過多次嘗試后,初始化的耗時一直在 10-15ms,這些足以說明函數(shù)式組件比有狀態(tài)組件有著更好的性能。

          結(jié)合場景使用 v-show 和 v-if

          以下是兩個使用 v-show 和 v-if 的模板

          <template>
          ??<div>
          ????<UserProfile?:user="user1"?v-if="visible"?/>
          ????<button?@click="visible?=?!visible">toggle</button>
          ??</div>
          </template>
          復(fù)制代碼
          <template>
          ??<div>
          ????<UserProfile?:user="user1"?v-show="visible"?/>
          ????<button?@click="visible?=?!visible">toggle</button>
          ??</div>
          </template>
          復(fù)制代碼

          這兩者的作用都是用來控制某些組件或 DOM 的顯示 / 隱藏,在討論它們的性能差異之前,先來分析下這兩者有何不同。其中,v-if 的模板會被編譯成:

          function?render()?{
          ??with?(this)?{
          ????return?_c(
          ??????'div',
          ??????[
          ????????visible
          ????????????_c('UserProfile',?{
          ??????????????attrs:?{
          ????????????????user:?user1
          ??????????????}
          ????????????})
          ??????????:?_e(),
          ????????_c(
          ??????????'button',
          ??????????{
          ????????????on:?{
          ??????????????click:?function?($event)?{
          ????????????????visible?=?!visible
          ??????????????}
          ????????????}
          ??????????},
          ??????????[_v('toggle')]
          ????????)
          ??????],
          ????)
          ??}
          }
          復(fù)制代碼

          可以看到,v-if 的部分被轉(zhuǎn)換成了一個三元表達(dá)式,visible 為 true 時,創(chuàng)建一個 UserProfile 的 vnode,否則創(chuàng)建一個空 vnode,在 patch 的時候,新舊節(jié)點不一樣,就會移除舊的節(jié)點或創(chuàng)建新的節(jié)點,這樣的話UserProfile也會跟著創(chuàng)建 / 銷毀。如果UserProfile組件里有很多 DOM,或者要執(zhí)行很多初始化 / 銷毀邏輯,那么隨著 visible 的切換,勢必會浪費掉很多性能。這個時候就可以用 v-show 進(jìn)行優(yōu)化,我們來看下 v-show 編譯后的代碼:

          function?render()?{
          ??with?(this)?{
          ????return?_c(
          ??????'div',
          ??????[
          ????????_c('UserProfile',?{
          ??????????directives:?[
          ????????????{
          ??????????????name:?'show',
          ??????????????rawName:?'v-show',
          ??????????????value:?visible,
          ??????????????expression:?'visible'
          ????????????}
          ??????????],
          ??????????attrs:?{
          ????????????user:?user1
          ??????????}
          ????????}),
          ????????_c(
          ??????????'button',
          ??????????{
          ????????????on:?{
          ??????????????click:?function?($event)?{
          ????????????????visible?=?!visible
          ??????????????}
          ????????????}
          ??????????},
          ??????????[_v('toggle')]
          ????????)
          ??????],
          ????)
          ??}
          }
          復(fù)制代碼

          v-show被編譯成了directives,實際上,v-show 是一個 Vue 內(nèi)部的指令,在這個指令的代碼中,主要執(zhí)行了以下邏輯:

          el.style.display?=?value???el.__vOriginalDisplay?:?'none'
          復(fù)制代碼

          它其實是通過切換元素的 display 屬性來控制的,和 v-if 相比,不需要在 patch 階段創(chuàng)建 / 移除節(jié)點,只是根據(jù)v-show上綁定的值來控制 DOM 元素的style.display屬性,在頻繁切換的場景下就可以節(jié)省很多性能。

          但是并不是說v-show可以在任何情況下都替換v-if,如果初始值是false時,v-if并不會創(chuàng)建隱藏的節(jié)點,但是v-show會創(chuàng)建,并通過設(shè)置style.display='none'來隱藏,雖然外表看上去這個 DOM 都是被隱藏的,但是v-show已經(jīng)完整的走了一遍創(chuàng)建的流程,造成了性能的浪費。

          所以,v-if的優(yōu)勢體現(xiàn)在初始化時,v-show體現(xiàn)在更新時,當(dāng)然并不是要求你絕對按照這個方式來,比如某些組件初始化時會請求數(shù)據(jù),而你想先隱藏組件,然后在顯示時能立刻看到數(shù)據(jù),這時候就可以用v-show,又或者你想每次顯示這個組件時都是最新的數(shù)據(jù),那么你就可以用v-if,所以我們要結(jié)合具體業(yè)務(wù)場景去選一個合適的方式。

          使用 keep-alive

          在動態(tài)組件的場景下:

          <template>
          ??<div>
          ????<component?:is="currentComponent"?/>
          ??</div>
          </template>
          復(fù)制代碼

          這個時候有多個組件來回切換,currentComponent每變一次,相關(guān)的組件就會銷毀 / 創(chuàng)建一次,如果這些組件比較復(fù)雜的話,就會造成一定的性能壓力,其實我們可以使用 keep-alive 將這些組件緩存起來:

          <template>
          ??<div>
          ????<keep-alive>
          ??????<component?:is="currentComponent"?/>
          ????</keep-alive>
          ??</div>
          </template>
          復(fù)制代碼

          keep-alive的作用就是將它包裹的組件在第一次渲染后就緩存起來,下次需要時就直接從緩存里面取,避免了不必要的性能浪費,在討論上個問題時,說的是v-show初始時性能壓力大,因為它要創(chuàng)建所有的組件,其實可以用keep-alive優(yōu)化下:

          <template>
          ??<div>
          ????<keep-alive>
          ??????<UserProfileA?v-if="visible"?/>
          ??????<UserProfileB?v-else?/>
          ????</keep-alive>
          ??</div>
          </template>
          復(fù)制代碼

          這樣的話,初始化時不會渲染UserProfileB組件,當(dāng)切換visible時,才會渲染UserProfileB組件,同時被keep-alive緩存下來,頻繁切換時,由于是直接從緩存中取,所以會節(jié)省很多性能,所以這種方式在初始化和更新時都有較好的性能。

          但是keep-alive并不是沒有缺點,組件被緩存時會占用內(nèi)存,屬于空間和時間上的取舍,在實際開發(fā)中要根據(jù)場景選擇合適的方式。

          避免 v-for 和 v-if 同時使用

          這一點是 Vue 官方的風(fēng)格指南中明確指出的一點:Vue 風(fēng)格指南

          如以下模板:

          <ul>
          ??<li?v-for="user?in?users"?v-if="user.isActive"?:key="user.id">
          ????{{?user.name?}}
          ??</li>
          </ul>
          復(fù)制代碼

          會被編譯成:

          //?簡化版
          function?render()?{
          ??return?_c(
          ????'ul',
          ????this.users.map((user)?=>?{
          ??????return?user.isActive
          ??????????_c(
          ????????????'li',
          ????????????{
          ??????????????key:?user.id
          ????????????},
          ????????????[_v(_s(user.name))]
          ??????????)
          ????????:?_e()
          ????}),
          ??)
          }
          復(fù)制代碼

          可以看到,這里是先遍歷(v-for),再判斷(v-if),這里有個問題就是:如果你有一萬條數(shù)據(jù),其中只有 100 條是isActive狀態(tài)的,你只希望顯示這 100 條,但是實際在渲染時,每一次渲染,這一萬條數(shù)據(jù)都會被遍歷一遍。比如你在這個組件內(nèi)的其他地方改變了某個響應(yīng)式數(shù)據(jù)時,會觸發(fā)重新渲染,調(diào)用渲染函數(shù),調(diào)用渲染函數(shù)時,就會執(zhí)行到上面的代碼,從而將這一萬條數(shù)據(jù)遍歷一遍,即使你的users沒有發(fā)生任何改變。

          為了避免這個問題,在此場景下你可以用計算屬性代替:

          <template>
          ??<div>
          ????<ul>
          ??????<li?v-for="user?in?activeUsers"?:key="user.id">{{?user.name?}}</li>
          ????</ul>
          ??</div>
          </template>

          <script>
          ??export?default?{
          ????//?...
          ????computed:?{
          ??????activeUsers()?{
          ????????return?this.users.filter((user)?=>?user.isActive)
          ??????}
          ????}
          ??}
          </script>
          復(fù)制代碼

          這樣只會在users發(fā)生改變時才會執(zhí)行這段遍歷的邏輯,和之前相比,避免了不必要的性能浪費。

          始終為 v-for 添加 key,并且不要將 index 作為的 key

          這一點是 Vue 風(fēng)格指南中明確指出的一點,同時也是面試時常問的一點,很多人都習(xí)慣的將 index 作為 key,這樣其實是不太好的,index 作為 key 時,將會讓 diff 算法產(chǎn)生錯誤的判斷,從而帶來一些性能問題,你可以看下 ssh 大佬的文章,深入分析下,為什么 Vue 中不要用 index 作為 key。在這里我也通過一個例子來簡單說明下當(dāng) index 作為 key 時是如何影響性能的。

          看下這個例子:

          const?Item?=?{
          ??name:?'Item',
          ??props:?['message',?'color'],
          ??render(h)?{
          ????debugger
          ????console.log('執(zhí)行了Item的render')
          ????return?h('div',?{?style:?{?color:?this.color?}?},?[this.message])
          ??}
          }

          new?Vue({
          ??name:?'Parent',
          ??template:?`
          ??<div?@click="reverse"?class="list">
          ????<Item
          ??????v-for="(item,index)?in?list"
          ??????:key="item.id"
          ??????:message="item.message"
          ??????:color="item.color"
          ????/>
          ??</div>`,
          ??components:?{?Item?},
          ??data()?{
          ????return?{
          ??????list:?[
          ????????{?id:?'a',?color:?'#f00',?message:?'a'?},
          ????????{?id:?'b',?color:?'#0f0',?message:?'b'?}
          ??????]
          ????}
          ??},
          ??methods:?{
          ????reverse()?{
          ??????this.list.reverse()
          ????}
          ??}
          }).$mount('#app')
          復(fù)制代碼

          這里有一個 list,會渲染出來a b,點擊后會執(zhí)行reverse方法將這個 list 顛倒下順序,你可以將這個例子復(fù)制下來,在自己的電腦上看下效果。

          我們先來分析用id作為 key 時,點擊時會發(fā)生什么,

          由于 list 發(fā)生了改變,會觸發(fā)Parent組件的重新渲染,拿到新的vnode,和舊的vnode去執(zhí)行patch,我們主要關(guān)心的就是patch過程中的updateChildren邏輯,updateChildren就是對新舊兩個children執(zhí)行diff算法,使盡可能地對節(jié)點進(jìn)行復(fù)用,對于我們這個例子而言,此時舊的children是:

          ;[
          ??{
          ????tag:?'Item',
          ????key:?'a',
          ????propsData:?{
          ??????color:?'#f00',
          ??????message:?'紅色'
          ????}
          ??},
          ??{
          ????tag:?'Item',
          ????key:?'b',
          ????propsData:?{
          ??????color:?'#0f0',
          ??????message:?'綠色'
          ????}
          ??}
          ]
          復(fù)制代碼

          執(zhí)行reverse后的新的children是:

          ;[
          ??{
          ????tag:?'Item',
          ????key:?'b',
          ????propsData:?{
          ??????color:?'#0f0',
          ??????message:?'綠色'
          ????}
          ??},
          ??{
          ????tag:?'Item',
          ????key:?'a',
          ????propsData:?{
          ??????color:?'#f00',
          ??????message:?'紅色'
          ????}
          ??}
          ]
          復(fù)制代碼

          此時執(zhí)行updateChildrenupdateChildren會對新舊兩組 children 節(jié)點的循環(huán)進(jìn)行對比:

          while?(oldStartIdx?<=?oldEndIdx?&&?newStartIdx?<=?newEndIdx)?{
          ??if?(isUndef(oldStartVnode))?{
          ????oldStartVnode?=?oldCh[++oldStartIdx]?//?Vnode?has?been?moved?left
          ??}?else?if?(isUndef(oldEndVnode))?{
          ????oldEndVnode?=?oldCh[--oldEndIdx]
          ??}?else?if?(sameVnode(oldStartVnode,?newStartVnode))?{
          ????//?對新舊節(jié)點執(zhí)行patchVnode
          ????//?移動指針
          ??}?else?if?(sameVnode(oldEndVnode,?newEndVnode))?{
          ????//?對新舊節(jié)點執(zhí)行patchVnode
          ????//?移動指針
          ??}?else?if?(sameVnode(oldStartVnode,?newEndVnode))?{
          ????//?對新舊節(jié)點執(zhí)行patchVnode
          ????//?移動oldStartVnode節(jié)點
          ????//?移動指針
          ??}?else?if?(sameVnode(oldEndVnode,?newStartVnode))?{
          ????//?對新舊節(jié)點執(zhí)行patchVnode
          ????//?移動oldEndVnode節(jié)點
          ????//?移動指針
          ??}?else?{
          ????//...
          ??}
          }
          復(fù)制代碼

          通過sameVnode判斷兩個節(jié)點是相同節(jié)點的話,就會執(zhí)行相應(yīng)的邏輯:

          function?sameVnode(a,?b)?{
          ??return?(
          ????a.key?===?b.key?&&
          ????((a.tag?===?b.tag?&&
          ??????a.isComment?===?b.isComment?&&
          ??????isDef(a.data)?===?isDef(b.data)?&&
          ??????sameInputType(a,?b))?||
          ??????(isTrue(a.isAsyncPlaceholder)?&&
          ????????a.asyncFactory?===?b.asyncFactory?&&
          ????????isUndef(b.asyncFactory.error)))
          ??)
          }
          復(fù)制代碼

          sameVnode主要就是通過 key 去判斷,由于我們顛倒了 list 的順序,所以第一輪對比中:sameVnode(oldStartVnode, newEndVnode)成立,即舊的首節(jié)點和新的尾節(jié)點是同一個節(jié)點,此時會執(zhí)行patchVnode邏輯,patchVnode中會執(zhí)行prePatchprePatch中會更新 props,此時我們的兩個節(jié)點的propsData是相同的,都為{color: '#0f0',message: '綠色'},這樣的話Item組件的 props 就不會更新,Item也不會重新渲染。再回到updateChildren中,會繼續(xù)執(zhí)行"移動oldStartVnode節(jié)點"的操作,將 DOM 元素。移動到正確位置,其他節(jié)點對比也是同樣的流程。

          可以發(fā)現(xiàn),在整個流程中,只是移動了節(jié)點,并沒有觸發(fā) Item 組件的重新渲染,這樣實現(xiàn)了節(jié)點的復(fù)用。

          我們再來看下使用index作為 key 的情況,使用index時,舊的children是:

          ;[
          ??{
          ????tag:?'Item',
          ????key:?0,
          ????propsData:?{
          ??????color:?'#f00',
          ??????message:?'紅色'
          ????}
          ??},
          ??{
          ????tag:?'Item',
          ????key:?1,
          ????propsData:?{
          ??????color:?'#0f0',
          ??????message:?'綠色'
          ????}
          ??}
          ]
          復(fù)制代碼

          執(zhí)行reverse后的新的children是:

          ;[
          ??{
          ????tag:?'Item',
          ????key:?0,
          ????propsData:?{
          ??????color:?'#0f0',
          ??????message:?'綠色'
          ????}
          ??},
          ??{
          ????tag:?'Item',
          ????key:?1,
          ????propsData:?{
          ??????color:?'#f00',
          ??????message:?'紅色'
          ????}
          ??}
          ]
          復(fù)制代碼

          這里和id作為 key 時的節(jié)點就有所不同了,雖然我們把 list 順序顛倒了,但是 key 的順序卻沒變,在updateChildrensameVnode(oldStartVnode, newStartVnode)將會成立,即舊的首節(jié)點和新的首節(jié)點相同,此時執(zhí)行patchVnode -> prePatch -> 更新props,這個時候舊的 propsData 是{color: '#f00',message: '紅色'},新的 propsData 是{color: '#0f0',message: '綠色'},更新過后,Item 的 props 將會發(fā)生改變,會觸發(fā) Item 組件的重新渲染

          這就是 index 作為 key 和 id 作為 key 時的區(qū)別,id 作為 key 時,僅僅是移動了節(jié)點,并沒有觸發(fā) Item 的重新渲染。index 作為 key 時,觸發(fā)了 Item 的重新渲染,可想而知,當(dāng) Item 是一個復(fù)雜的組件時,必然會引起性能問題。

          上面的流程比較復(fù)雜,涉及的也比較多,可以拆開寫好幾篇文章,有些地方我只是簡略的說了一下,如果你不是很明白的話,你可以把上面的例子復(fù)制下來,在自己的電腦上調(diào)式,我在 Item 的渲染函數(shù)中加了打印日志和 debugger,你可以分別用 id 和 index 作為 key 嘗試下,你會發(fā)現(xiàn) id 作為 key 時,Item 的渲染函數(shù)沒有執(zhí)行,但是 index 作為 key 時,Item 的渲染函數(shù)執(zhí)行了,這就是這兩種方式的區(qū)別。

          延遲渲染

          延遲渲染就是分批渲染,假設(shè)我們某個頁面里有一些組件在初始化時需要執(zhí)行復(fù)雜的邏輯:

          <template>
          ??<div>
          ????<!--?Heavy組件初始化時需要執(zhí)行很復(fù)雜的邏輯,執(zhí)行大量計算?-->
          ????<Heavy1?/>
          ????<Heavy2?/>
          ????<Heavy3?/>
          ????<Heavy4?/>
          ??</div>
          </template>
          復(fù)制代碼

          這將會占用很長時間,導(dǎo)致幀數(shù)下降、卡頓,其實可以使用分批渲染的方式來進(jìn)行優(yōu)化,就是先渲染一部分,再渲染另一部分:

          參考黃軼老師揭秘 Vue.js 九個性能優(yōu)化技巧中的代碼:

          <template>
          ??<div>
          ????<Heavy?v-if="defer(1)"?/>
          ????<Heavy?v-if="defer(2)"?/>
          ????<Heavy?v-if="defer(3)"?/>
          ????<Heavy?v-if="defer(4)"?/>
          ??</div>
          </template>

          <script>
          export?default?{
          ??data()?{
          ????return?{
          ??????displayPriority:?0
          ????}
          ??},
          ??mounted()?{
          ????this.runDisplayPriority()
          ??},
          ??methods:?{
          ????runDisplayPriority()?{
          ??????const?step?=?()?=>?{
          ????????requestAnimationFrame(()?=>?{
          ??????????this.displayPriority++
          ??????????if?(this.displayPriority?<?10)?{
          ????????????step()
          ??????????}
          ????????})
          ??????}
          ??????step()
          ????},
          ????defer(priority)?{
          ??????return?this.displayPriority?>=?priority
          ????}
          ??}
          }
          </script>

          復(fù)制代碼

          其實原理很簡單,主要是維護(hù)displayPriority變量,通過requestAnimationFrame在每一幀渲染時自增,然后我們就可以在組件上通過v-if="defer(n)"使displayPriority增加到某一值時再渲染,這樣就可以避免 js 執(zhí)行時間過長導(dǎo)致的卡頓問題了。

          使用非響應(yīng)式數(shù)據(jù)

          在 Vue 組件初始化數(shù)據(jù)時,會遞歸遍歷在 data 中定義的每一條數(shù)據(jù),通過Object.defineProperty將數(shù)據(jù)改成響應(yīng)式,這就意味著如果 data 中的數(shù)據(jù)量很大的話,在初始化時將會使用很長的時間去執(zhí)行Object.defineProperty, 也就會帶來性能問題,這個時候我們可以強制使數(shù)據(jù)變?yōu)榉琼憫?yīng)式,從而節(jié)省時間,看下這個例子:

          <template>
          ??<div>
          ????<ul>
          ??????<li?v-for="item?in?heavyData"?:key="item.id">{{?item.name?}}</li>
          ????</ul>
          ??</div>
          </template>

          <script>
          //?一萬條數(shù)據(jù)
          const?heavyData?=?Array(10000)
          ??.fill(null)
          ??.map((_,?idx)?=>?({?name:?'test',?message:?'test',?id:?idx?}))

          export?default?{
          ??data()?{
          ????return?{
          ??????heavyData:?heavyData
          ????}
          ??},
          ??beforeCreate()?{
          ????this.start?=?Date.now()
          ??},
          ??created()?{
          ????console.log(Date.now()?-?this.start)
          ??}
          }
          </script>
          復(fù)制代碼

          heavyData中有一萬條數(shù)據(jù),這里統(tǒng)計了下從beforeCreatecreated經(jīng)歷的時間,對于這個例子而言,這個時間基本上就是初始化數(shù)據(jù)的時間。

          我在我個人的電腦上多次測試,這個時間一直在40-50ms,然后我們通過Object.freeze()方法,將heavyData變?yōu)榉琼憫?yīng)式的再試下:

          //...
          data()?{
          ??return?{
          ????heavyData:?Object.freeze(heavyData)
          ??}
          }
          //...
          復(fù)制代碼

          改完之后再試下,初始化數(shù)據(jù)的時間變成了0-1ms,快了有40ms,這40ms都是遞歸遍歷heavyData執(zhí)行Object.defineProperty的時間。

          那么,為什么Object.freeze()會有這樣的效果呢?對某一對象使用Object.freeze()后,將不能向這個對象添加新的屬性,不能刪除已有屬性,不能修改該對象已有屬性的可枚舉性、可配置性、可寫性,以及不能修改已有屬性的值。

          而 Vue 在將數(shù)據(jù)改造成響應(yīng)式之前有個判斷:

          export?function?observe(value,?asRootData)?{
          ??//?...省略其他邏輯
          ??if?(
          ????shouldObserve?&&
          ????!isServerRendering()?&&
          ????(Array.isArray(value)?||?isPlainObject(value))?&&
          ????Object.isExtensible(value)?&&
          ????!value._isVue
          ??)?{
          ????ob?=?new?Observer(value)
          ??}
          ??//?...省略其他邏輯
          }
          復(fù)制代碼

          這個判斷條件中有一個Object.isExtensible(value),這個方法是判斷一個對象是否是可擴(kuò)展的,由于我們使用了Object.freeze(),這里肯定就返回了false,所以就跳過了下面的過程,自然就省了很多時間。

          實際上,不止初始化數(shù)據(jù)時有影響,你可以用上面的例子統(tǒng)計下從createdmounted所用的時間,在我的電腦上不使用Object.freeze()時,這個時間是60-70ms,使用Object.freeze()后降到了40-50ms,這是因為在渲染函數(shù)中讀取heavyData中的數(shù)據(jù)時,會執(zhí)行到通過Object.defineProperty定義的getter方法,Vue 在這里做了一些收集依賴的處理,肯定就會占用一些時間,由于使用了Object.freeze()后的數(shù)據(jù)是非響應(yīng)式的,沒有了收集依賴的過程,自然也就節(jié)省了性能。

          由于訪問響應(yīng)式數(shù)據(jù)會走到自定義 getter 中并收集依賴,所以平時使用時要避免頻繁訪問響應(yīng)式數(shù)據(jù),比如在遍歷之前先將這個數(shù)據(jù)存在局部變量中,尤其是在計算屬性、渲染函數(shù)中使用,關(guān)于這一點更具體的說明,你可以看黃奕老師的這篇文章:Local variables

          但是這樣做也不是沒有任何問題的,這樣會導(dǎo)致heavyData下的數(shù)據(jù)都不是響應(yīng)式數(shù)據(jù),你對這些數(shù)據(jù)使用computedwatch等都不會產(chǎn)生效果,不過通常來說這種大量的數(shù)據(jù)都是展示用的,如果你有特殊的需求,你可以只對這種數(shù)據(jù)的某一層使用Object.freeze(),同時配合使用上文中的延遲渲染、函數(shù)式組件等,可以極大提升性能。

          模板編譯和渲染函數(shù)、JSX 的性能差異

          Vue 項目不僅可以使用 SFC 的方式開發(fā),也可以使用渲染函數(shù)或 JSX 開發(fā),很多人認(rèn)為僅僅是只是開發(fā)方式不同,卻不知這些開發(fā)方式之間也有性能差異,甚至差異很大,這一節(jié)我就找些例子來說明下,希望你以后在選擇開發(fā)方式時有更多衡量的標(biāo)準(zhǔn)。

          其實 Vue2 模板編譯中的性能優(yōu)化不多,Vue3 中有很多,Vue3 通過編譯和運行時結(jié)合的方式提升了很大的性能,但是由于本篇文章講的是 Vue2 的性能優(yōu)化,并且 Vue2 現(xiàn)在還是有很多人在使用,所以我就挑 Vue2 模板編譯中的一點來說下。

          靜態(tài)節(jié)點

          下面這個模板:

          <div>你好!?<span>Hello</span></div>
          復(fù)制代碼

          會被編譯成:

          function?render()?{
          ??with?(this)?{
          ????return?_m(0)
          ??}
          }
          復(fù)制代碼

          可以看到和普通的渲染函數(shù)是有些不一樣的,下面我們來看下為什么會編譯成這樣的代碼。

          Vue 的編譯會經(jīng)過optimize過程,這個過程中會標(biāo)記靜態(tài)節(jié)點,具體內(nèi)容可以看黃奕老師寫的這個文檔:Vue2 編譯 - optimize 標(biāo)記靜態(tài)節(jié)點。

          codegen階段判斷到靜態(tài)節(jié)點的標(biāo)記會走到genStatic的分支:

          function?genStatic(el,?state)?{
          ??el.staticProcessed?=?true
          ??const?originalPreState?=?state.pre
          ??if?(el.pre)?{
          ????state.pre?=?el.pre
          ??}
          ??state.staticRenderFns.push(`with(this){return?${genElement(el,?state)}}`)
          ??state.pre?=?originalPreState
          ??return?`_m(${state.staticRenderFns.length?-?1}${
          ????el.staticInFor???',true'?:?''
          ??})`
          }
          復(fù)制代碼

          這里就是生成代碼的關(guān)鍵邏輯,這里會把渲染函數(shù)保存在staticRenderFns里,然后拿到當(dāng)前值的下標(biāo)生成_m函數(shù),這就是為什么我們會得到_m(0)

          這個_m其實是renderStatic的縮寫:

          export?function?renderStatic(index,?isInFor)?{
          ??const?cached?=?this._staticTrees?||?(this._staticTrees?=?[])
          ??let?tree?=?cached[index]
          ??if?(tree?&&?!isInFor)?{
          ????return?tree
          ??}
          ??tree?=?cached[index]?=?this.$options.staticRenderFns[index].call(
          ????this._renderProxy,
          ????null,
          ????this
          ??)
          ??markStatic(tree,?`__static__${index}`,?false)
          ??return?tree
          }

          function?markStatic(tree,?key)?{
          ??if?(Array.isArray(tree))?{
          ????for?(let?i?=?0;?i?<?tree.length;?i++)?{
          ??????if?(tree[i]?&&?typeof?tree[i]?!==?'string')?{
          ????????markStaticNode(tree[i],?`${key}_${i}`,?isOnce)
          ??????}
          ????}
          ??}?else?{
          ????markStaticNode(tree,?key,?isOnce)
          ??}
          }

          function?markStaticNode(node,?key,?isOnce)?{
          ??node.isStatic?=?true
          ??node.key?=?key
          ??node.isOnce?=?isOnce
          }
          復(fù)制代碼

          renderStatic的內(nèi)部實現(xiàn)比較簡單,先是獲取到組件實例的_staticTrees,如果沒有就創(chuàng)建一個,然后嘗試從_staticTrees上獲取之前緩存的節(jié)點,獲取到的話就直接返回,否則就從staticRenderFns上獲取到對應(yīng)的渲染函數(shù)執(zhí)行并將結(jié)果緩存到_staticTrees上,這樣下次再進(jìn)入這個函數(shù)時就會直接從緩存上返回結(jié)果。

          拿到節(jié)點后還會通過markStatic將節(jié)點打上isStatic等標(biāo)記,標(biāo)記為isStatic的節(jié)點會直接跳過patchVnode階段,因為靜態(tài)節(jié)點是不會變的,所以也沒必要 patch,跳過 patch 可以節(jié)省性能。

          通過編譯和運行時結(jié)合的方式,可以幫助我們很好的提升應(yīng)用性能,這是渲染函數(shù) / JSX 很難達(dá)到的,當(dāng)然不是說不能用 JSX,相比于模板,JSX 更加靈活,兩者有各自的使用場景。在這里寫這些是希望能給你提供一些技術(shù)選型的標(biāo)準(zhǔn)。

          Vue2 的編譯優(yōu)化除了靜態(tài)節(jié)點,還有插槽,createElement 等。

          Vue3 的模板編譯優(yōu)化

          相比于 Vue2,Vue3 中的模板編譯優(yōu)化更加突出,性能提升的更多,由于涉及的比較多,本篇文章寫不下,如果你感興趣的話你可以看看這些文章:Vue3 Compiler 優(yōu)化細(xì)節(jié),如何手寫高性能渲染函數(shù),聊聊 Vue.js 3.0 的模板編譯優(yōu)化,以及尤雨溪的解讀視頻:Vue 之父尤雨溪深度解讀 Vue3.0 的開發(fā)思路,以后我也會單獨寫一些文章分析 Vue3 的模板編譯優(yōu)化。

          總結(jié)

          希望你能通過這篇文章了解一些常見的 Vue 性能優(yōu)化方式并理解其背后的原理,在日常開發(fā)中不僅要能寫出代碼,還要能知道這樣寫的好處 / 壞處是什么,避免寫出容易產(chǎn)生性能問題的代碼。

          這篇文章的內(nèi)容并不是全部的優(yōu)化方式。除了文章涉及的這些,還有打包優(yōu)化、異步加載,懶加載等等。性能優(yōu)化并不是一下子就完成的,需要你結(jié)合項目分析出性能瓶頸,找到問題并解決,在這個過程中,你肯定能發(fā)掘出更多優(yōu)化方式。

          最后,這篇文章寫了很長時間,花費了很多精力,如果你覺得對你有幫助的話,麻煩點個贊?,支持下,感謝!

          相關(guān)推薦

          以下是本文有參考或者相關(guān)的文章:

          1. 還在看那些老掉牙的性能優(yōu)化文章么?這些最新性能指標(biāo)了解下
          2. 揭秘 Vue.js 九個性能優(yōu)化技巧
          3. Vue 應(yīng)用性能優(yōu)化指南
          4. 為什么 Vue 中不要用 index 作為 key?(diff 算法詳解)
          5. Vue2 編譯 - optimize 標(biāo)記靜態(tài)節(jié)點
          6. Vue3 Compiler 優(yōu)化細(xì)節(jié),如何手寫高性能渲染函數(shù)
          7. Vue2.6 針對插槽的性能優(yōu)化
          8. 聊聊 Vue.js 3.0 的模板編譯優(yōu)化
          9. 「前端進(jìn)階」高性能渲染十萬條數(shù)據(jù) (時間分片)
          10. Vue 之父尤雨溪深度解讀 Vue3.0 的開發(fā)思路

          以下是可以實時查看編譯結(jié)果的工具:

          1. Vue2 Template Explorer
          2. Vue3 Template Explorer

          最后



          如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:

          1. 點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-)

          2. 歡迎加我微信「?sherlocked_93?」拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

          3. 關(guān)注公眾號「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時聊騷。


          b88ec5fa7846204e54a017bae9ab622a.webp點個在看支持我吧,轉(zhuǎn)發(fā)就更好了


          瀏覽 82
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  俺去也WWW在线视频 | 久久午夜无码鲁丝午夜精品 | 欧美三级片一区二区 | 国产免费A片 | 无码内射国产 |