這 22 個 Vue3 的實用技巧,你可能還不知道!

代碼絕不止能跑就行,但是廢話只說一句:碼字不易求個👍,🙇‍🙇‍🙇‍。

演示代碼使用 Vue3 + ts + Vite 編寫,但是也會列出適用於 Vue2 的優化技巧,如果某個優化只適用於 Vue3 或者 Vue2,我會在標題中標出來。

代碼優化

v-for 中使用 key

使用 v-for 更新已渲染的元素列表時,默認用就地複用策略;列表數據修改的時候,他會根據 key 值去判斷某個值是否修改,如果修改,則重新渲染這一項,否則複用之前的元素;

使用 key 的注意事項:

何時使用何種 key?

這是一個非常有考究的問題,首先你要知道 vue 中的 原地複用 (大概就是 虛擬dom 變化時,兩個 虛擬dom節點key 如果一樣就不會重新創建節點,而是修改原來的節點)

當我們渲染的數據不需要保持狀態時,例如常見的單純的表格分頁渲染(不包含輸入,只是展示)、下拉加載更多等場景,那麼使用 index 作爲 key 再好不過,因爲進入下一頁或者上一頁時就會原地複用之前的節點,而不是重新創建,如果使用唯一的 id 作爲 key 反而會重新創建 dom,性能相對較低。

此外使用 index 作爲 key 我還應該要儘量避免對數組的中間進行 增加 / 刪除 等會影響後面元素 key 變化的操作。這會讓 vue 認爲後面所有元素都發生了變化,導致多餘的對比和原地複用。

所以使用 index 作爲 key 需要滿足:

  1. 數據沒有獨立的狀態

  2. 數據不會進行 增加 / 刪除 等會影響後面元素 key 變化的操作

哪何時使用 id 作爲 key 呢?

對於大多數數據的 id 都是唯一的,這無疑的一個 key 的優選答案。對於任何大多數情況使用 id 作爲 key 都不會出現上面 bug。但是如果你需要考慮性能問題,那就就要思考是否應該使用原地複用了。

同樣是上面的分頁數據展示,如果使用 id 作爲 key ,可想而知每一頁的每一條數據 id 都是不一樣的,所以當換頁時兩顆 虛擬DOM樹 的節點的 key 完全不一致,vue 就會移除原來的節點然後創建新的節點。可想而知效率會更加低下。但是他也有它的優點。唯一的 key 可以幫助 diff 更加精確的爲我們綁定狀態,這尤其適合數據有獨立的狀態的場景,例如帶輸入框或者單選框的列表數據。

所以何時使用 id 作爲 key?只有一點:

  1. 無法使用 index 作爲 key 的時候

v-if/v-else-if/v-else 中使用 key

可能很多人都會忽略這個點

原因:默認情況下,Vue 會盡可能高效的更新 DOM。這意味着其在相同類型的元素之間切換時,會修補已存在的元素,而不是將舊的元素移除然後在同一位置添加一個新元素。如果本不相同的元素被識別爲相同,則會出現意料之外的副作用。

如果只有一個 v-if ,沒有 v-else 或者 v-if-else 的話,就沒有必要加 key 了

相對於 v-for 的 key, v-if/v-else-if/v-else 中的 key 相對簡單,我們可以直接寫入固定的字符串或者數組即可

  <transition>
    <button 
      v-if="isEditing"
      v-on:click="isEditing = false"
    >
      Save
    </button>
    <button 
      v-else 
      v-on:click="isEditing = true"
    >
      Edit
    </button>
  </transition>
複製代碼
.v-enter-active, .v-leave-active {
  transition: all 1s;
}
.v-enter, .v-leave-to {
  opacity: 0;
  transform: translateY(30px);
}
.v-leave-active {
  position: absolute;
}
複製代碼

例如對於上面的代碼, 你會發現雖然對 button 添加了 過渡效果, 但是如果不添加 key 切換時是無法觸發過渡的

v-for 和 v-if 不要一起使用(Vue2)

此優化技巧僅限於 Vue2,Vue3 中對 v-for 和 v-if 的優先級做了調整

這個大家都知道

永遠不要把 v-if 和 v-for 同時用在同一個元素上。 引至 Vue2.x 風格指南 [1]

原因是 v-for 的 優先級高於 v-if,所以當它們使用再同一個標籤上是,每一個渲染都會先循環再進行條件判斷

注意: Vue3 中 v-if 優先級高於 v-for,所以當 v-forv-if 一起使用時效果類似於 Vue2 中把 v-if 上提的效果

例如下面這段代碼在 Vue2 中是不被推薦的,Vue 也會給出對應的警告

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

我們應該儘量將 v-if 移動到上級 或者 使用 計算屬性來處理數據

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

如果你不想讓循環的內容多出一個無需有的上級容器,那麼你可以選擇使用 template 來作爲其父元素,template 不會被瀏覽器渲染爲 DOM 節點

如果我想要判斷遍歷對象裏面每一項的內容來選擇渲染的數據的話,可以使用 computed 來對遍歷對象進行過濾

// js
let usersActive = computed(()=>users.filter(user => user.active))

// template
<ul>
    <li v-for="user in usersActive">
      {{ user.name }}
    </li>
</ul>
複製代碼

合理的選擇 v-if 和 v-show

v-ifv-show 的區別相比大家都非常熟悉了;v-if 通過直接操作 DOM 的刪除和添加來控制元素的顯示和隱藏;v-show 是通過控制 DOM 的 display CSS 熟悉來控制元素的顯示和隱藏

由於對 DOM 的 添加 / 刪除 操作性能遠遠低於操作 DOM 的 CSS 屬性

所以當元素需要頻繁的 顯示 / 隱藏 變化時,我們使用 v-show 來提高性能。

當元素不需要頻繁的 顯示 / 隱藏 變化時,我們通過 v-if 來移除 DOM 可以節約掉瀏覽器渲染這個的一部分 DOM 需要的資源

使用簡單的 計算屬性

應該把複雜計算屬性分割爲儘可能多的更簡單的 property。

  • 易於測試

    當每個計算屬性都包含一個非常簡單且很少依賴的表達式時,撰寫測試以確保其正確工作就會更加容易。

  • 易於閱讀

    簡化計算屬性要求你爲每一個值都起一個描述性的名稱,即便它不可複用。這使得其他開發者 (以及未來的你) 更容易專注在他們關心的代碼上並搞清楚發生了什麼。

  • 更好的 “擁抱變化”

    任何能夠命名的值都可能用在視圖上。舉個例子,我們可能打算展示一個信息,告訴用戶他們存了多少錢;也可能打算計算稅費,但是可能會分開展現,而不是作爲總價的一部分。

    小的、專注的計算屬性減少了信息使用時的假設性限制,所以需求變更時也用不着那麼多重構了。

引至 Vue2 風格指南 [2]

computed 大家後很熟悉, 它會在其表達式中依賴的響應式數據發送變化時重新計算。如果我們在一個計算屬性中書寫了比較複雜的表達式,那麼其依賴的響應式數據也任意變得更多。當其中任何一個依賴項變化時整個表達式都需要重新計算

let price = computed(()=>{
  let basePrice = manufactureCost / (1 - profitMargin)
  return (
      basePrice -
      basePrice * (discountPercent || 0)
  )
})
複製代碼

當 manufactureCost、profitMargin、discountPercent 中任何一個變化時都會重新計算整個 price。

但是如果我們改成下面這樣

let basePrice = computed(() => manufactureCost / (1 - profitMargin))
let discount = computed(() => basePrice * (discountPercent || 0))
let finalPrice = computed(() => basePrice - discount)
複製代碼

如果當 discountPercent 變化時,只會 重新計算 discount 和 finalPrice,由於 computed緩存特性,不會重新計算 basePrice

functional 函數式組件(Vue2)

注意,這僅僅在 Vue2 中被作爲一種優化手段,在 3.x 中,有狀態組件和函數式組件之間的性能差異已經大大減少,並且在大多數用例中是微不足道的。因此,在 SFCs 上使用 functional 的開發人員的遷移路徑是刪除該 attribute,並將 props 的所有引用重命名爲 $props,將 attrs 重命名爲 $attrs

優化前

<template> 
    <div class="cell"> 
        <div v-if="value" class="on"></div> 
        <section v-else class="off"></section> 
    </div> 
</template> 

<script> 
export default { 
    props: ['value']} 
</script>
複製代碼

優化後

<template functional> 
    <div class="cell"> 
        <div v-if="props.value" class="on"></div> 
        <section v-else class="off"></section> 
    </div> 
</template> 

<script> 
export default { 
    props: ['value']} 
</script>
複製代碼

拆分組件

什麼?你寫的一個 vue 文件有一千多行代碼?🤔

合理的拆分組件不僅僅可以優化性能,還能夠讓代碼更清晰可讀。單一功能原則嘛

源自 slides.com/akryum/vuec…[3]

優化前

<template>
  <div :style="{ opacity: number / 300 }">
    <div>{{ heavy() }}</div>
  </div>
</template>

<script>
export default {
  props: ['number'],
  methods: {
    heavy () { /* HEAVY TASK */ }
  }
}
</script>
複製代碼

優化後

<template>
  <div :style="{ opacity: number / 300 }">
    <ChildComp/>
  </div>
</template>

<script>
export default {
  props: ['number'],
  components: {
    ChildComp: {
      methods: {
        heavy () { /* HEAVY TASK */ }
      },
      render (h) {
        return h('div', this.heavy())
      }
    }
  }
}
</script>
複製代碼

由於 Vue 的更新是組件粒度的,雖然每一幀都通過數據修改導致了父組件的重新渲染,但是 ChildComp 卻不會重新渲染,因爲它的內部也沒有任何響應式數據的變化。所以優化後的組件不會在每次渲染都執行耗時任務

使用局部變量

優化前

<template>
  <div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>

<script>
import { heavy } from '@/utils'

export default {
  props: ['start'],
  computed: {
    base () { return 42 },
    result () {
      let result = this.start
      for (let i = 0; i < 1000; i++) {
        result += heavy(this.base)
      }
      return result
    }
  }
}
</script>
複製代碼

優化後

<template>
  <div :style="{ opacity: start / 300 }">
    {{ result }}</div>
</template>

<script>
import { heavy } from '@/utils'

export default {
  props: ['start'],
  computed: {
    base () { return 42 },
    result () {
      const base = this.base
      let result = this.start
      for (let i = 0; i < 1000; i++) {
        result += heavy(base)
      }
      return result
    }
  }
}
</script>
複製代碼

這裏主要是優化前後的組件的計算屬性 result 的實現差異,優化前的組件多次在計算過程中訪問 this.base,而優化後的組件會在計算前先用局部變量 base,緩存 this.base,後面直接訪問 base

那麼爲啥這個差異會造成性能上的差異呢,原因是你每次訪問 this.base 的時候,由於 this.base 是一個響應式對象,所以會觸發它的 getter,進而會執行依賴收集相關邏輯代碼。類似的邏輯執行多了,像示例這樣,幾百次循環更新幾百個組件,每個組件觸發 computed 重新計算,然後又多次執行依賴收集相關邏輯,性能自然就下降了。

從需求上來說,this.base 執行一次依賴收集就夠了,把它的 getter 求值結果返回給局部變量 base,後續再次訪問 base 的時候就不會觸發 getter,也不會走依賴收集的邏輯了,性能自然就得到了提升。

引至 揭祕 Vue.js 九個性能優化技巧 [4]

使用 KeepAlive

在一些渲染成本比較高的組件需要被經常切換時,可以使用 keep-alive 來緩存這個組件

而在使用 keep-alive 後,被 keep-alive 包裹的組件在經過第一次渲染後,的 vnode 以及 DOM 都會被緩存起來,然後再下一次再次渲染該組件的時候,直接從緩存中拿到對應的 vnode 和 DOM,然後渲染,並不需要再走一次組件初始化,renderpatch 等一系列流程,減少了 script 的執行時間,性能更好。

注意: 濫用 keep-alive 只會讓你的應用變得更加卡頓,因爲他會長期佔用較大的內存

事件的銷燬

當一個組件被銷燬時,我們應該清除組件中添加的 全局事件 和 定時器 等來防止內存泄漏

Vue3 的 HOOK 可以讓我們將事件的聲明和銷燬寫在一起,更加可讀

function scrollFun(){ /* ... */}
document.addEventListener("scroll", scrollFun)

onBeforeUnmount(()=>{
  document.removeEventListener("scroll", scrollFun)
})
複製代碼

Vue2 依然可以通過 $once 來做到這樣的效果,當然你也可以在 optionsAPI beforeDestroy 中銷燬事件,但是我更加推薦前者的寫法,因爲後者會讓相同功能的代碼更分散

function scrollFun(){ /* ... */}
document.addEventListener("scroll", scrollFun)

this.$once('hook:beforeDestroy'()=>{
  document.removeEventListener("scroll", scrollFun)
})
複製代碼
function scrollFun(){ /* ... */}

export default {
  created() {
    document.addEventListener("scroll", scrollFun)
  },
  beforeDestroy(){
    document.removeEventListener("scroll", scrollFun)
  }
}
複製代碼

圖片加載

圖片懶加載:適用於頁面上有較多圖片且並不是所有圖片都在一屏中展示的情況,vue-lazyload 插件給我們提供了一個很方便的圖片懶加載指令 v-lazy

但是並不是所有圖片都適合使用懶加載,例如 banner、相冊等 更加推薦使用圖片預加載技術,將當前展示圖片的前一張和後一張優先下載。

使用合適的圖片類型

使用 webp 格式:這個沒什麼好說的,大家都知道 WebP 的優勢體現在它具有更優的圖像數據壓縮算法, 能帶來更小的圖片體積, 而且擁有肉眼識別無差異的圖像質量;同時具備了無損和有損的壓縮模式、Alpha 透明以及動畫的特性, 在 JPEG 和 PNG 上的轉化效果都相當優秀、穩定和統一。

使用交錯 GIF 或者是漸進 JPEG:還有一種優化用戶體驗的方式,就是使用交錯 GIF 或者是漸進(Progressive Encoding)JPEG 的圖片。漸進 JPEG 文件首先是模糊的,然後漸漸清晰起來。

Baseline JPEG 和 Progressive JPEG 的區別:

JPEG 文件格式有兩種保存方式。他們是 Baseline JPEG 和 Progressive JPEG。

兩種格式有相同尺寸以及圖像數據,他們的擴展名也是相同的,唯一的區別是二者顯示的方式不同。

Baseline JPEG.webp

Progressive JPEG.webp

Progressive JPEG 的優點:

減少不必要的響應式數據

大家都知道 vue 中響應式數據需要額外的對其綁定 get、get 處理函數,如果你的某些數據不會發生變化或者你不希望它的變化會導致任何副作用(更新視圖或者其他)。一般我會這樣定義他。

export default {
  data() {
    this.version = '10'; // 不會被做響應式處理
    return {
        /* ... */
    }
  }
}
複製代碼

Tips: 其實這種方式並不是最好的,因爲這會將數據直接綁定到 vue 實例上,而 vue 更希望數據能夠統一在 data 中,然後通過代理到 vue 實例上的方式來訪問。所以更好的方式應該是對 data 中的數據進行凍結。

在 Vue3 中無法通過以上方式來解決,因爲 Proxy 代理的粒度是整個對象而不是某一個屬性。

採用合理的數據處理算法

這個相對比較考驗 數據結構和算法 的功底

例如一個將數組轉化爲多級結構的方法

/**
 * 數組轉樹形結構,時間複雜度O(n)
 * @param list 數組
 * @param idKey 元素id鍵
 * @param parIdKey 元素父id鍵
 * @param parId 第一級根節點的父id值
 * @return {[]}
 */
function listToTree (list,idKey,parIdKey,parId) {
    let map = {};
    let result = [];
    let len = list.length;

    // 構建map
    for (let i = 0; i < len; i++) {
        //將數組中數據轉爲鍵值對結構 (這裏的數組和obj會相互引用,這是算法實現的重點)
        map[list[i][idKey]] = list[i];
    }

    // 構建樹形數組
    for(let i=0; i < len; i++) {
        let itemParId = list[i][parIdKey];
        // 頂級節點
        if(itemParId === parId) {
            result.push(list[i]);
            continue;
        }
        // 孤兒節點,捨棄(不存在其父節點)
        if(!map[itemParId]){
            continue;
        }
        // 將當前節點插入到父節點的children中(由於是引用數據類型,obj中對於節點變化,result中對應節點會跟着變化)
        if(map[itemParId].children) {
            map[itemParId].children.push(list[i]);
        } else {
            map[itemParId].children = [list[i]];
        }
    }
    return result;
}
複製代碼

其他

除了上面說的方法以外還有很多優化技巧,只是我在項目並不是太常用🤣

首屏 / 體積優化

我在項目中關於首屏優化主要有以下幾個優化方向

體積優化

代碼分割

代碼分割的作用的將打包產物分割爲一個一個的小產物,其依賴 esModule。所以當你使用 import() 函數來導入一個文件或者依賴,那麼這個文件或者依賴就會被單獨打包爲一個小產物。路由懶加載異步組件 都是使用這個原理。

對於 UI 庫 我一般不會使用按需加載組件,而是比較喜歡 CDN 引入的方式來優化。

網絡

CDN: 首先就是上面的說的 CDN 引入把,開發階段使用本地庫,通過配置 外部擴展(Externals) 打包時來排除這些依賴。然後在 html 文件中通過 CDN 的方式來引入它們

Server Push: HTTP2 已經相對成熟了;經過上面的 CDN 引入,我們可以對網站使用 HTTP2 的 Server Push 功能來讓瀏覽器提前加載 這些 CDN 和 其他文件。

開啓 gzip: 這個上面已經說過了,其原理就是當客戶端和服務端都支持 gzip 傳輸時,服務端會優先發送經過 gzip 壓縮過的文件,然後客戶端接收到在進行解壓。

開啓緩存: 一般我使用的是協商緩存,但是這並不適用於所有情況,例如對於使用了 Server Push 的文件,就不能隨意的修改其文件名。所以我一般還會將生產的主要文件固定文件名

用戶體驗優化

我們可以在覈心文件加載完成之前,通過展示 loading 或者骨架屏等方式來提升用戶體驗。即可縮短白屏時間。

但是需要注意的是,頁面剛開始加載時有許多資源需要加載,如果將 loading 相關的資源放到 dom 後的話,有可能會導致 loading 的資源被其他資源阻塞。

所以推薦 loading 相關的 css 或者 js 代碼最好是內聯到 html 中的頭部,這樣即可保證展示 loading 時對應的 css 和 js 已經加載完成。並且不推薦 loading 中使用高性能或者高網絡消化的邏輯,這樣會延長後面其他資源的解析或者加載時間。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2xC85Kzjvh1AIytDQR3DFA