1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 18290字,需瀏覽 37分鐘

         ·

        2021-06-30 15:13

        前言

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

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

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

        我們先來(lái)看下這兩種語(yǔ)法分別會(huì)被編譯成什么:

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

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

        會(huì)被編譯成:

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

        使用舊的寫法,對(duì)于以下模板:

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

        會(huì)被編譯成:

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

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

        這樣一來(lái),舊的寫法在更新時(shí)就多了一個(gè)父組件更新的過(guò)程,而新的寫法由于直接更新子組件,就會(huì)更加高效,性能更好,所以推薦始終使用v-slot:slotName語(yǔ)法。

        使用計(jì)算屬性

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

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

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

        使用函數(shù)式組件

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

        我們可以寫一個(gè)簡(jiǎn)單的 demo 來(lái)驗(yàn)證下這個(gè)優(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('用時(shí):',?Date.now()?-?this.start)
        ????}
        ??}
        </script>

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

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

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

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

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

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

        以下是兩個(gè)使用 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ù)制代碼

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

        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)換成了一個(gè)三元表達(dá)式,visible 為 true 時(shí),創(chuàng)建一個(gè) UserProfile 的 vnode,否則創(chuàng)建一個(gè)空 vnode,在 patch 的時(shí)候,新舊節(jié)點(diǎn)不一樣,就會(huì)移除舊的節(jié)點(diǎn)或創(chuàng)建新的節(jié)點(diǎn),這樣的話UserProfile也會(huì)跟著創(chuàng)建 / 銷毀。如果UserProfile組件里有很多 DOM,或者要執(zhí)行很多初始化 / 銷毀邏輯,那么隨著 visible 的切換,勢(shì)必會(huì)浪費(fèi)掉很多性能。這個(gè)時(shí)候就可以用 v-show 進(jìn)行優(yōu)化,我們來(lái)看下 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,實(shí)際上,v-show 是一個(gè) Vue 內(nèi)部的指令,在這個(gè)指令的代碼中,主要執(zhí)行了以下邏輯:

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

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

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

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

        使用 keep-alive

        在動(dòng)態(tài)組件的場(chǎng)景下:

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

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

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

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

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

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

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

        避免 v-for 和 v-if 同時(shí)使用

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

        如以下模板:

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

        會(huì)被編譯成:

        //?簡(jiǎn)化版
        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),這里有個(gè)問(wèn)題就是:如果你有一萬(wàn)條數(shù)據(jù),其中只有 100 條是isActive狀態(tài)的,你只希望顯示這 100 條,但是實(shí)際在渲染時(shí),每一次渲染,這一萬(wàn)條數(shù)據(jù)都會(huì)被遍歷一遍。比如你在這個(gè)組件內(nèi)的其他地方改變了某個(gè)響應(yīng)式數(shù)據(jù)時(shí),會(huì)觸發(fā)重新渲染,調(diào)用渲染函數(shù),調(diào)用渲染函數(shù)時(shí),就會(huì)執(zhí)行到上面的代碼,從而將這一萬(wàn)條數(shù)據(jù)遍歷一遍,即使你的users沒(méi)有發(fā)生任何改變。

        為了避免這個(gè)問(wèn)題,在此場(chǎng)景下你可以用計(jì)算屬性代替:

        <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ù)制代碼

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

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

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

        看下這個(gè)例子:

        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ù)制代碼

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

        我們先來(lái)分析用id作為 key 時(shí),點(diǎn)擊時(shí)會(huì)發(fā)生什么,

        由于 list 發(fā)生了改變,會(huì)觸發(fā)Parent組件的重新渲染,拿到新的vnode,和舊的vnode去執(zhí)行patch,我們主要關(guān)心的就是patch過(guò)程中的updateChildren邏輯,updateChildren就是對(duì)新舊兩個(gè)children執(zhí)行diff算法,使盡可能地對(duì)節(jié)點(diǎn)進(jìn)行復(fù)用,對(duì)于我們這個(gè)例子而言,此時(shí)舊的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ù)制代碼

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

        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))?{
        ????//?對(duì)新舊節(jié)點(diǎn)執(zhí)行patchVnode
        ????//?移動(dòng)指針
        ??}?else?if?(sameVnode(oldEndVnode,?newEndVnode))?{
        ????//?對(duì)新舊節(jié)點(diǎn)執(zhí)行patchVnode
        ????//?移動(dòng)指針
        ??}?else?if?(sameVnode(oldStartVnode,?newEndVnode))?{
        ????//?對(duì)新舊節(jié)點(diǎn)執(zhí)行patchVnode
        ????//?移動(dòng)oldStartVnode節(jié)點(diǎn)
        ????//?移動(dòng)指針
        ??}?else?if?(sameVnode(oldEndVnode,?newStartVnode))?{
        ????//?對(duì)新舊節(jié)點(diǎn)執(zhí)行patchVnode
        ????//?移動(dòng)oldEndVnode節(jié)點(diǎn)
        ????//?移動(dòng)指針
        ??}?else?{
        ????//...
        ??}
        }
        復(fù)制代碼

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

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

        我們?cè)賮?lái)看下使用index作為 key 的情況,使用index時(shí),舊的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 時(shí)的節(jié)點(diǎn)就有所不同了,雖然我們把 list 順序顛倒了,但是 key 的順序卻沒(méi)變,在updateChildren時(shí)sameVnode(oldStartVnode, newStartVnode)將會(huì)成立,即舊的首節(jié)點(diǎn)和新的首節(jié)點(diǎn)相同,此時(shí)執(zhí)行patchVnode -> prePatch -> 更新props,這個(gè)時(shí)候舊的 propsData 是{color: '#f00',message: '紅色'},新的 propsData 是{color: '#0f0',message: '綠色'},更新過(guò)后,Item 的 props 將會(huì)發(fā)生改變,會(huì)觸發(fā) Item 組件的重新渲染。

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

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

        延遲渲染

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

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

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

        參考黃軼老師揭秘 Vue.js 九個(gè)性能優(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ù)制代碼

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

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

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

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

        <script>
        //?一萬(wàn)條數(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中有一萬(wàn)條數(shù)據(jù),這里統(tǒng)計(jì)了下從beforeCreatecreated經(jīng)歷的時(shí)間,對(duì)于這個(gè)例子而言,這個(gè)時(shí)間基本上就是初始化數(shù)據(jù)的時(shí)間。

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

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

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

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

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

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

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

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

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

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

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

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

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

        靜態(tài)節(jié)點(diǎn)

        下面這個(gè)模板:

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

        會(huì)被編譯成:

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

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

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

        codegen階段判斷到靜態(tài)節(jié)點(diǎn)的標(biāo)記會(huì)走到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)鍵邏輯,這里會(huì)把渲染函數(shù)保存在staticRenderFns里,然后拿到當(dāng)前值的下標(biāo)生成_m函數(shù),這就是為什么我們會(huì)得到_m(0)。

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

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

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

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

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

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

        總結(jié)

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

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

        最后,這篇文章寫了很長(zhǎng)時(shí)間,花費(fèi)了很多精力,如果你覺(jué)得對(duì)你有幫助的話,麻煩點(diǎn)個(gè)贊?,支持下,感謝!

        相關(guān)推薦

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

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

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

        1. Vue2 Template Explorer
        2. Vue3 Template Explorer

        最后



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

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

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

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


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


        瀏覽 82
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产又色又爽又黄又免费 | 天天干夜夜操911 | 国产成人精品a v久久 | 18禁蜜桃 | 又黄又爽在线观看 | 成人免费视频 观看视频 | 影音先锋啪啪 | 4虎海外cvt4wd最新版本更新内容 | 中文字幕日韩精品欧美一区蜜桃网 | 天天拍天天干 |