萬字長文!總結Vue性能優化方式及原理

前言

咱們在使用 Vue 或其餘框架的平常開發中,或多或少的都會遇到一些性能問題,儘管 Vue 內部已經幫助咱們作了許多優化,可是仍是有些問題是須要咱們主動去避免的。我在個人平常開中,以及網上各類大佬的文章中總結了一些容易產生性能問題的場景以及針對這些問題優化的技巧,這篇文章就來探討下,但願對你有所幫助。javascript

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

v-slot是 2.6 新增的語法,具體可查看:Vue2.6,2.6 發佈已是快兩年前的事情了,可是如今仍然有很多人仍然在使用slot="slotName"這個語法。雖然這兩個語法都能達到相同的效果,可是內部的邏輯確實不同的,下面來看下這兩種方式有什麼不一樣之處。html

咱們先來看下這兩種語法分別會被編譯成什麼:前端

使用新的寫法,對於父組件中的如下模板:vue

<child>
  <template v-slot:name>{{name}}</template>
</child>
複製代碼

會被編譯成:java

function render() {
  with (this) {
    return _c('child', {
      scopedSlots: _u([
        {
          key: 'name',
          fn: function () {
            return [_v(_s(name))]
          },
          proxy: true
        }
      ])
    })
  }
}
複製代碼

使用舊的寫法,對於如下模板:node

<child>
  <template slot="name">{{name}}</template>
</child>
複製代碼

會被編譯成:git

function render() {
  with (this) {
    return _c(
      'child',
      [
        _c(
          'template',
          {
            slot: 'name'
          },
          [_v(_s(name))]
        )
      ],
      2
    )
  }
}
複製代碼

經過編譯後的代碼能夠發現,舊的寫法是將插槽內容做爲 children 渲染的,會在父組件的渲染函數中建立,插槽內容的依賴會被父組件收集(name 的 dep 收集到父組件的渲染 watcher),而新的寫法將插槽內容放在了 scopedSlots 中,會在子組件的渲染函數中調用,插槽內容的依賴會被子組件收集(name 的 dep 收集到子組件的渲染 watcher),最終致使的結果就是:當咱們修改 name 這個屬性時,舊的寫法是調用父組件的更新(調用父組件的渲染 watcher),而後在父組件更新過程當中調用子組件更新(prePatch => updateChildComponent),而新的寫法則是直接調用子組件的更新(調用子組件的渲染 watcher)。github

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

使用計算屬性

這一點已經被說起不少次了,計算屬性最大的一個特色就是它是能夠被緩存的,這個緩存指的是隻要它的依賴的不發生改變,它就不會被從新求值,再次訪問時會直接拿到緩存的值,在作一些複雜的計算時,能夠極大提高性能。能夠看如下代碼:算法

<template>
  <div>{{superCount}}</div>
</template>
<script> export default { data() { return { count: 1 } }, computed: { superCount() { let superCount = this.count // 假設這裏有個複雜的計算 for (let i = 0; i < 10000; i++) { superCount++ } return superCount } } } </script>
複製代碼

這個例子中,在 created、mounted 以及模板中都訪問了 superCount 屬性,這三次訪問中,實際上只有第一次即created時纔會對 superCount 求值,因爲 count 屬性並未改變,其他兩次都是直接返回緩存的 value,對於計算屬性更加詳細的介紹能夠看我以前寫的文章:Vue computed 是如何實現的?

使用函數式組件

對於某些組件,若是咱們只是用來顯示一些數據,不須要管理狀態,監聽數據等,那麼就能夠用函數式組件。函數式組件是無狀態的,無實例的,在初始化時不須要初始化狀態,不須要建立實例,也不須要去處理生命週期等,相比有狀態組件,會更加輕量,同時性能也更好。具體的函數式組件使用方式可參考官方文檔:函數式組件

咱們能夠寫一個簡單的 demo 來驗證下這個優化:

// 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" :name="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>
複製代碼

UserProfile 這個組件只渲染了 props 的 name,而後在 App.vue 中調用 500 次,統計從 beforeMount 到 mounted 的耗時,即爲 500 個子組件(UserProfile)初始化的耗時。

通過我屢次嘗試後,發現耗時一直在 30ms 左右,那麼如今咱們再把改爲 UserProfile 改爲函數式組件:

<template functional>
  <div class="user-profile">{{ props.name }}</div>
</template>
複製代碼

此時再通過屢次嘗試後,初始化的耗時一直在 10-15ms,這些足以說明函數式組件比有狀態組件有着更好的性能。

結合場景使用 v-show 和 v-if

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

<template>
  <div>
    <UserProfile :user="user1" v-if="visible" />
    <button @click="visible = !visible">toggle</button>
  </div>
</template>
複製代碼
<template>
  <div>
    <UserProfile :user="user1" v-show="visible" />
    <button @click="visible = !visible">toggle</button>
  </div>
</template>
複製代碼

這二者的做用都是用來控制某些組件或 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')]
        )
      ],
      1
    )
  }
}
複製代碼

能夠看到,v-if 的部分被轉換成了一個三元表達式,visible 爲 true 時,建立一個 UserProfile 的 vnode,不然建立一個空 vnode,在 patch 的時候,新舊節點不同,就會移除舊的節點或建立新的節點,這樣的話UserProfile也會跟着建立/銷燬。若是UserProfile組件裏有不少 DOM,或者要執行不少初始化/銷燬邏輯,那麼隨着 visible 的切換,勢必會浪費掉不少性能。這個時候就能夠用 v-show 進行優化,咱們來看下 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')]
        )
      ],
      1
    )
  }
}
複製代碼

v-show被編譯成了directives,實際上,v-show 是一個 Vue 內部的指令,在這個指令的代碼中,主要執行了如下邏輯:

el.style.display = value ? el.__vOriginalDisplay : 'none'
複製代碼

它實際上是經過切換元素的 display 屬性來控制的,和 v-if 相比,不須要在 patch 階段建立/移除節點,只是根據v-show上綁定的值來控制 DOM 元素的style.display屬性,在頻繁切換的場景下就能夠節省不少性能。

可是並非說v-show能夠在任何狀況下都替換v-if,若是初始值是false時,v-if並不會建立隱藏的節點,可是v-show會建立,並經過設置style.display='none'來隱藏,雖然外表看上去這個 DOM 都是被隱藏的,可是v-show已經完整的走了一遍建立的流程,形成了性能的浪費。

因此,v-if的優點體如今初始化時,v-show體如今更新時,固然並非要求你絕對按照這個方式來,好比某些組件初始化時會請求數據,而你想先隱藏組件,而後在顯示時能馬上看到數據,這時候就能夠用v-show,又或者你想每次顯示這個組件時都是最新的數據,那麼你就能夠用v-if,因此咱們要結合具體業務場景去選一個合適的方式。

使用 keep-alive

在動態組件的場景下:

<template>
  <div>
    <component :is="currentComponent" />
  </div>
</template>
複製代碼

這個時候有多個組件來回切換,currentComponent每變一次,相關的組件就會銷燬/建立一次,若是這些組件比較複雜的話,就會形成必定的性能壓力,其實咱們可使用 keep-alive 將這些組件緩存起來:

<template>
  <div>
    <keep-alive>
      <component :is="currentComponent" />
    </keep-alive>
  </div>
</template>
複製代碼

keep-alive的做用就是將它包裹的組件在第一次渲染後就緩存起來,下次須要時就直接從緩存裏面取,避免了沒必要要的性能浪費,在討論上個問題時,說的是v-show初始時性能壓力大,由於它要建立全部的組件,其實能夠用keep-alive優化下:

<template>
  <div> <keep-alive> <UserProfileA v-if="visible" /> <UserProfileB v-else /> </keep-alive> </div>
</template>
複製代碼

這樣的話,初始化時不會渲染UserProfileB組件,當切換visible時,纔會渲染UserProfileB組件,同時被keep-alive緩存下來,頻繁切換時,因爲是直接從緩存中取,因此會節省不少性能,因此這種方式在初始化和更新時都有較好的性能。

可是keep-alive並非沒有缺點,組件被緩存時會佔用內存,屬於空間和時間上的取捨,在實際開發中要根據場景選擇合適的方式。

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

這一點是 Vue 官方的風格指南中明確指出的一點:Vue 風格指南

如如下模板:

<ul>
  <li v-for="user in users" v-if="user.isActive" :key="user.id">
    {{ user.name }}
  </li>
</ul>
複製代碼

會被編譯成:

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

能夠看到,這裏是先遍歷(v-for),再判斷(v-if),這裏有個問題就是:若是你有一萬條數據,其中只有 100 條是isActive狀態的,你只但願顯示這 100 條,可是實際在渲染時,每一次渲染,這一萬條數據都會被遍歷一遍。好比你在這個組件內的其餘地方改變了某個響應式數據時,會觸發從新渲染,調用渲染函數,調用渲染函數時,就會執行到上面的代碼,從而將這一萬條數據遍歷一遍,即便你的users沒有發生任何改變。

爲了不這個問題,在此場景下你能夠用計算屬性代替:

<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>
複製代碼

這樣只會在users發生改變時纔會執行這段遍歷的邏輯,和以前相比,避免了沒必要要的性能浪費。

始終爲 v-for 添加 key,而且不要將 index 做爲的 key

這一點是Vue 風格指南中明確指出的一點,同時也是面試時常問的一點,不少人都習慣的將 index 做爲 key,這樣實際上是不太好的,index 做爲 key 時,將會讓 diff 算法產生錯誤的判斷,從而帶來一些性能問題,你能夠看下ssh大佬的文章,深刻分析下,爲何 Vue 中不要用 index 做爲 key。在這裏我也經過一個例子來簡單說明下當 index 做爲 key 時是如何影響性能的。

看下這個例子:

const Item = {
  name: 'Item',
  props: ['message', 'color'],
  render(h) {
    debugger
    console.log('執行了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')
複製代碼

這裏有一個 list,會渲染出來a b,點擊後會執行reverse方法將這個 list 顛倒下順序,你能夠將這個例子複製下來,在本身的電腦上看下效果。

咱們先來分析用id做爲 key 時,點擊時會發生什麼,

因爲 list 發生了改變,會觸發Parent組件的從新渲染,拿到新的vnode,和舊的vnode去執行patch,咱們主要關心的就是patch過程當中的updateChildren邏輯,updateChildren就是對新舊兩個children執行diff算法,使盡量地對節點進行復用,對於咱們這個例子而言,此時舊的children是:

;[
  {
    tag: 'Item',
    key: 'a',
    propsData: {
      color: '#f00',
      message: '紅色'
    }
  },
  {
    tag: 'Item',
    key: 'b',
    propsData: {
      color: '#0f0',
      message: '綠色'
    }
  }
]
複製代碼

執行reverse後的新的children是:

;[
  {
    tag: 'Item',
    key: 'b',
    propsData: {
      color: '#0f0',
      message: '綠色'
    }
  },
  {
    tag: 'Item',
    key: 'a',
    propsData: {
      color: '#f00',
      message: '紅色'
    }
  }
]
複製代碼

此時執行updateChildrenupdateChildren會對新舊兩組 children 節點的循環進行對比:

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)) {
    // 對新舊節點執行patchVnode
    // 移動指針
  } else if (sameVnode(oldEndVnode, newEndVnode)) {
    // 對新舊節點執行patchVnode
    // 移動指針
  } else if (sameVnode(oldStartVnode, newEndVnode)) {
    // 對新舊節點執行patchVnode
    // 移動oldStartVnode節點
    // 移動指針
  } else if (sameVnode(oldEndVnode, newStartVnode)) {
    // 對新舊節點執行patchVnode
    // 移動oldEndVnode節點
    // 移動指針
  } else {
    //...
  }
}
複製代碼

經過sameVnode判斷兩個節點是相同節點的話,就會執行相應的邏輯:

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)))
  )
}
複製代碼

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

能夠發現,在整個流程中,只是移動了節點,並無觸發 Item 組件的從新渲染,這樣實現了節點的複用。

咱們再來看下使用index做爲 key 的狀況,使用index時,舊的children是:

;[
  {
    tag: 'Item',
    key: 0,
    propsData: {
      color: '#f00',
      message: '紅色'
    }
  },
  {
    tag: 'Item',
    key: 1,
    propsData: {
      color: '#0f0',
      message: '綠色'
    }
  }
]
複製代碼

執行reverse後的新的children是:

;[
  {
    tag: 'Item',
    key: 0,
    propsData: {
      color: '#0f0',
      message: '綠色'
    }
  },
  {
    tag: 'Item',
    key: 1,
    propsData: {
      color: '#f00',
      message: '紅色'
    }
  }
]
複製代碼

這裏和id做爲 key 時的節點就有所不一樣了,雖然咱們把 list 順序顛倒了,可是 key 的順序卻沒變,在updateChildrensameVnode(oldStartVnode, newStartVnode)將會成立,即舊的首節點和新的首節點相同,此時執行patchVnode -> prePatch -> 更新props,這個時候舊的 propsData 是{color: '#f00',message: '紅色'},新的 propsData 是{color: '#0f0',message: '綠色'},更新事後,Item 的 props 將會發生改變,會觸發 Item 組件的從新渲染

這就是 index 做爲 key 和 id 做爲 key 時的區別,id 做爲 key 時,僅僅是移動了節點,並無觸發 Item 的從新渲染。index 做爲 key 時,觸發了 Item 的從新渲染,可想而知,當 Item 是一個複雜的組件時,必然會引發性能問題。

上面的流程比較複雜,涉及的也比較多,能夠拆開寫好幾篇文章,有些地方我只是簡略的說了一下,若是你不是很明白的話,你能夠把上面的例子複製下來,在本身的電腦上調式,我在 Item 的渲染函數中加了打印日誌和 debugger,你能夠分別用 id 和 index 做爲 key 嘗試下,你會發現 id 做爲 key 時,Item 的渲染函數沒有執行,可是 index 做爲 key 時,Item 的渲染函數執行了,這就是這兩種方式的區別。

延遲渲染

延遲渲染就是分批渲染,假設咱們某個頁面裏有一些組件在初始化時須要執行復雜的邏輯:

<template>
  <div>
    <!-- Heavy組件初始化時須要執行很複雜的邏輯,執行大量計算 -->
    <Heavy1 />
    <Heavy2 />
    <Heavy3 />
    <Heavy4 />
  </div>
</template>
複製代碼

這將會佔用很長時間,致使幀數降低、卡頓,其實可使用分批渲染的方式來進行優化,就是先渲染一部分,再渲染另外一部分:

參考黃軼老師揭祕 Vue.js 九個性能優化技巧中的代碼:

<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>

複製代碼

其實原理很簡單,主要是維護displayPriority變量,經過requestAnimationFrame在每一幀渲染時自增,而後咱們就能夠在組件上經過v-if="defer(n)"使displayPriority增長到某一值時再渲染,這樣就能夠避免 js 執行時間過長致使的卡頓問題了。

使用非響應式數據

在 Vue 組件初始化數據時,會遞歸遍歷在 data 中定義的每一條數據,經過Object.defineProperty將數據改爲響應式,這就意味着若是 data 中的數據量很大的話,在初始化時將會使用很長的時間去執行Object.defineProperty,也就會帶來性能問題,這個時候咱們能夠強制使數據變爲非響應式,從而節省時間,看下這個例子:

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

<script> // 一萬條數據 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>
複製代碼

heavyData中有一萬條數據,這裏統計了下從beforeCreatecreated經歷的時間,對於這個例子而言,這個時間基本上就是初始化數據的時間。

我在我我的的電腦上屢次測試,這個時間一直在40-50ms,而後咱們經過Object.freeze()方法,將heavyData變爲非響應式的再試下:

//...
data() {
  return {
    heavyData: Object.freeze(heavyData)
  }
}
//...
複製代碼

改完以後再試下,初始化數據的時間變成了0-1ms,快了有40ms,這40ms都是遞歸遍歷heavyData執行Object.defineProperty的時間。

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

而 Vue 在將數據改形成響應式以前有個判斷:

export function observe(value, asRootData) {
  // ...省略其餘邏輯
  if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  // ...省略其餘邏輯
}
複製代碼

這個判斷條件中有一個Object.isExtensible(value),這個方法是判斷一個對象是不是可擴展的,因爲咱們使用了Object.freeze(),這裏確定就返回了false,因此就跳過了下面的過程,天然就省了不少時間。

實際上,不止初始化數據時有影響,你能夠用上面的例子統計下從createdmounted所用的時間,在個人電腦上不使用Object.freeze()時,這個時間是60-70ms,使用Object.freeze()後降到了40-50ms,這是由於在渲染函數中讀取heavyData中的數據時,會執行到經過Object.defineProperty定義的getter方法,Vue 在這裏作了一些收集依賴的處理,確定就會佔用一些時間,因爲使用了Object.freeze()後的數據是非響應式的,沒有了收集依賴的過程,天然也就節省了性能。

因爲訪問響應式數據會走到自定義 getter 中並收集依賴,因此平時使用時要避免頻繁訪問響應式數據,好比在遍歷以前先將這個數據存在局部變量中,尤爲是在計算屬性、渲染函數中使用,關於這一點更具體的說明,你能夠看黃奕老師的這篇文章:Local variables

可是這樣作也不是沒有任何問題的,這樣會致使heavyData下的數據都不是響應式數據,你對這些數據使用computedwatch等都不會產生效果,不過一般來講這種大量的數據都是展現用的,若是你有特殊的需求,你能夠只對這種數據的某一層使用Object.freeze(),同時配合使用上文中的延遲渲染、函數式組件等,能夠極大提高性能。

模板編譯和渲染函數、JSX 的性能差別

Vue 項目不只可使用 SFC 的方式開發,也可使用渲染函數或 JSX 開發,不少人認爲僅僅是隻是開發方式不一樣,殊不知這些開發方式之間也有性能差別,甚至差別很大,這一節我就找些例子來講明下,但願你之後在選擇開發方式時有更多衡量的標準。

其實 Vue2 模板編譯中的性能優化很少,Vue3 中有不少,Vue3 經過編譯和運行時結合的方式提高了很大的性能,可是因爲本篇文章講的是 Vue2 的性能優化,而且 Vue2 如今仍是有不少人在使用,因此我就挑 Vue2 模板編譯中的一點來講下。

靜態節點

下面這個模板:

<div>你好! <span>Hello</span></div>
複製代碼

會被編譯成:

function render() {
  with (this) {
    return _m(0)
  }
}
複製代碼

能夠看到和普通的渲染函數是有些不同的,下面咱們來看下爲何會編譯成這樣的代碼。

Vue 的編譯會通過optimize過程,這個過程當中會標記靜態節點,具體內容能夠看黃奕老師寫的這個文檔:Vue2 編譯 - optimize 標記靜態節點

codegen階段判斷到靜態節點的標記會走到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' : '' })`
}
複製代碼

這裏就是生成代碼的關鍵邏輯,這裏會把渲染函數保存在staticRenderFns裏,而後拿到當前值的下標生成_m函數,這就是爲何咱們會獲得_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
}
複製代碼

renderStatic的內部實現比較簡單,先是獲取到組件實例的_staticTrees,若是沒有就建立一個,而後嘗試從_staticTrees上獲取以前緩存的節點,獲取到的話就直接返回,不然就從staticRenderFns上獲取到對應的渲染函數執行並將結果緩存到_staticTrees上,這樣下次再進入這個函數時就會直接從緩存上返回結果。

拿到節點後還會經過markStatic將節點打上isStatic等標記,標記爲isStatic的節點會直接跳過patchVnode階段,由於靜態節點是不會變的,因此也不必 patch,跳過 patch 能夠節省性能。

經過編譯和運行時結合的方式,能夠幫助咱們很好的提高應用性能,這是渲染函數/JSX 很難達到的,固然不是說不能用 JSX,相比於模板,JSX 更加靈活,二者有各自的使用場景。在這裏寫這些是但願能給你提供一些技術選型的標準。

Vue2 的編譯優化除了靜態節點,還有插槽,createElement 等。

Vue3 的模板編譯優化

相比於 Vue2,Vue3 中的模板編譯優化更加突出,性能提高的更多,因爲涉及的比較多,本篇文章寫不下,若是你感興趣的話你能夠看看這些文章:Vue3 Compiler 優化細節,如何手寫高性能渲染函數聊聊 Vue.js 3.0 的模板編譯優化,以及尤雨溪的解讀視頻:Vue 之父尤雨溪深度解讀 Vue3.0 的開發思路,之後我也會單獨寫一些文章分析Vue3的模板編譯優化。

總結

但願你能經過這篇文章瞭解一些常見的Vue性能優化方式並理解其背後的原理,在平常開發中不只要能寫出代碼,還要能知道這樣寫的好處/壞處是什麼,避免寫出容易產生性能問題的代碼。

這篇文章的內容並非所有的優化方式。除了文章涉及的這些,還有打包優化、異步加載,懶加載等等。性能優化並非一會兒就完成的,須要你結合項目分析出性能瓶頸,找到問題並解決,在這個過程當中,你確定能發掘出更多優化方式。

最後,這篇文章寫了很長時間,花費了不少精力,若是你以爲對你有幫助的話,麻煩點個贊⭐,支持下,感謝!

相關推薦

如下是本文有參考或者相關的文章:

  1. 還在看那些老掉牙的性能優化文章麼?這些最新性能指標瞭解下
  2. 揭祕 Vue.js 九個性能優化技巧
  3. Vue 應用性能優化指南
  4. 爲何 Vue 中不要用 index 做爲 key?(diff 算法詳解)
  5. Vue2 編譯 - optimize 標記靜態節點
  6. Vue3 Compiler 優化細節,如何手寫高性能渲染函數
  7. Vue2.6 針對插槽的性能優化
  8. 聊聊 Vue.js 3.0 的模板編譯優化
  9. 「前端進階」高性能渲染十萬條數據(時間分片)
  10. Vue 之父尤雨溪深度解讀 Vue3.0 的開發思路

如下是能夠實時查看編譯結果的工具:

  1. Vue2 Template Explorer
  2. Vue3 Template Explorer

最後再求個贊⭐,感謝~

相關文章
相關標籤/搜索