Vue 優化速查

ssshooter發表於2022-02-25

首發傳送門:https://ssshooter.com/2021-03...

拆分元件

我也曾以為,拆分子元件是用於抽象,但實踐告訴我,拆分子元件是提升效能的一種方式(特定情況)。

在我的實際工作中遇到這麼個問題,有一個很大的表格,裡面有多個新增條目的對話方塊,當資料很多的時候,填寫新增資料都會變卡。

原因就是,在一個元件裡,修改值會造成整個元件的資料檢查和 diff。但是明知道大表單什麼都沒改,我還要浪費時間檢查個啥呢?

為了解決這個問題,把對話方塊單獨抽出來就成了十分有效的優化方法。

在子元件更新時,預設是不會觸發父元件更新的,除非子元件改變了父元件的資料。

我就以 element UI 的 dialog 舉例吧:

開啟此連結直接開啟可執行事例

寫一個頁面,裡面包含兩個 dialog,一個是直接寫到頁面中,另一個抽象為元件。

<template>
  <div>
    <el-button type="text" @click="dialogVisible = true"
      >點選開啟 Dialog</el-button
    >
    <el-button type="text" @click="dialogData.visible = true"
      >點選開啟 Dialog2</el-button
    >
    <div>{{ renderAfter() }}</div>
    <el-dialog
      :append-to-body="true"
      title="提示"
      :visible.sync="dialogVisible"
      width="30%"
    >
      <el-input v-model="xxx" />
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="dialogVisible = false"
          >確 定</el-button
        >
      </span>
    </el-dialog>
    <my-dialog :dialogData="dialogData" />
  </div>
</template>

<script>
  import MyDialog from './Dialog'
  export default {
    components: { MyDialog },
    name: 'HelloWorld',
    props: {
      msg: String,
    },
    data() {
      return {
        xxx: '',
        dialogVisible: false,
        dialogData: {
          visible: false,
        },
      }
    },
    methods: {
      renderAfter() {
        if (!window.renderCountTech) window.renderCountTech = 1
        else window.renderCountTech++
        console.log(
          '%c渲染函式檢測',
          'color:#fff;background:red',
          window.renderCountTech
        )
      },
    },
  }
</script>

以下是 dialog 元件的內容:

<template>
  <el-dialog
    :append-to-body="true"
    title="提示"
    :visible.sync="dialogData.visible"
    width="30%"
  >
    <el-input v-model="xxx" />
    <span slot="footer" class="dialog-footer">
      <el-button @click="dialogData.visible = false">取 消</el-button>
      <el-button type="primary" @click="dialogData.visible = false"
        >確 定</el-button
      >
    </span>
  </el-dialog>
</template>

<script>
  export default {
    props: ['dialogData'],
    data() {
      return {
        xxx: '',
      }
    },
  }
</script>

實踐可知,在 dialog 開啟關閉、以及輸入框修改資料時,會觸發整個元件的渲染函式,而在 dialog2 無論開啟關閉或輸入時都不會觸發父元件更新。在對話方塊所在元件的資料量少的話確實差別不大,但是量大的時候在對話方塊輸入的時候會有可感知的卡頓。(一句話:對話方塊自成一個元件,內部更新不影響父元件)

不止如此,反過來說,父元件更新的時候會渲染 dialog1 而不會渲染 dialog2,實在是一舉兩得。(一句話:父元件更新時不改變沒有資料變化的子元件)

即使這個元件不復用,也可以把對話方塊用到的方法分離到單獨檔案,不用和主頁面的方法混到一起。如果一個 dialog 有一大堆邏輯的話,分離到單獨檔案絕對是一個不錯的方法。

不過缺點當然也有:

首先,資料互動有點不方便,不過總能活用 $parent 和 Vuex 等方式解決。

第二個問題是修改 dialogData.visible 時會報錯 Unexpected mutation of "dialogData" prop. (vue/no-mutating-props)

作為 Vue 的最佳實踐,父給子的 prop 不得由子直接修改。我的觀點是如果你知道自己在做什麼,而且副作用不強的話……這樣做大概也無妨,不知道大家的意見如何呢?

如果堅持最佳實踐,有兩個方案:

emit

老實地用 on 和 emit,多些幾句程式碼:

<text-document v-bind:title.sync="doc.title"></text-document>

這麼寫等於:

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>

然後再在子元件通過 this.$emit('update:title', newTitle) 更新父元件。

$refs

可見性由 dialog 自己控制,在父元件通過 refs 設定 dialog 的可見性:

this.$refs.child.visible = true

反過來呢?你也可以在子裡修改父(這就不算是修改 prop 了,奇技淫巧)

this.$parent.visible = true

關於更新粒度更詳細的解釋可以看這裡:Vue 和 React 對於元件的更新粒度有什麼區別

PS:另外有一個隱藏結論,一個元件用了 slot 的話,子元件是會隨著父元件重新渲染的

computed

經過官方文件的強調,可以說是眾所周知,computed 可以快取計算結果,在依賴的值不變時減少計算消耗時間。

同樣眾所周知的還有 KeepAlive 元件。

減少使用重型元件和外掛

很多 UI 框架十分完善,但就是因為太完善,各種功能相互作用可能會讓你執行過慢或者不好 debug。

直接改原有框架當然可以,但是理解成本也不低,而且改完了,更新之後要合併程式碼更是難受,所以我更傾向於自己做一個。

我不願意做一個全世界都適用的輪子。因為每個人的需求都是不同的,如果要做一個滿足所有人的要求的輪子,這個輪子就會變得很重,我個人不太願意看到……

所以我希望做的“輪子”是一個輕量化的,滿足最小功能要求的“框架”,這樣大家修改也方便,同時也不必擔心隨著不斷更新越來越重。例如 v-vld 和 [tbl]。(https://github.com/v-cpn/cpn-tbl)

單向繫結甚至完全不繫結

MDN Object.defineProperty()

單向繫結是指資料使用 definePropertyconfigurable 設定成 false,這樣使資料相應化的 defineReactive 會跳過響應式設定:

Object.defineProperty(data, key, {
  configurable: false,
})

但是你仍然可以通過 v-model 向繫結的目標賦值,只是賦值後介面不會更新。

這種做法在資料巢狀很深時有起效,阻斷 defineReactive 後不會遞迴處理資料裡的子物件。(資料扁平化也可以免除遞迴)

完全不繫結就是官網寫的 Object.freeze 一個物件再賦值,這麼做物件內部的值(第一層)就直接不能改了,可以應用於純展示的資料。

快取 ajax 資料

可以封裝得像跟普通 axios 的 get 一樣,直接替換原來的 axios 物件:

import axios from 'axios'
import router from './router'
import { Message } from 'element-ui'
let baseURL = process.env.VUE_APP_BASEURL
let ajax = axios.create({
  baseURL,
  withCredentials: true,
})
let ajaxCache = {}

ajaxCache.get = (...params) => {
  let url = params[0]
  let option = params[1]
  let id = baseURL + url + (option ? JSON.stringify(option.params) : '')
  if (sessionStorage[id]) {
    return Promise.resolve(JSON.parse(sessionStorage[id]))
  }
  return ajax.get(...params)
}

ajax.interceptors.response.use(
  function (response) {
    // 其他處理
    // ……
    if (response.data.code === '20000') {
      let params = response.config.params
      let id = response.config.url + (params ? JSON.stringify(params) : '')
      sessionStorage[id] = JSON.stringify(response.data.data)
      return response.data.data
    }
  },
  function (error) {
    Message.error('連線超時')
    return Promise.reject(error)
  }
)

export default ajaxCache

函式式元件

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

https://codesandbox.io/s/func...

PS:函式式元件因為沒有例項化,所以每次使用都會重新渲染,想要完全靜態要用 v-once

PS2:在 Vue3 中,functional 和普通元件速度差別幾乎可以忽略

減少使用 this

簡單來說就是要注意 computed、watch 和 render 裡面每一次 this 取值的代價都包含依賴收集的程式碼,實際上這些程式碼只要執行一次就足夠了。

{
  computed: {
    base () {
      return 42
    },
    result ({ base, start }) {
      let result = start
      for (let i = 0; i < 1000; i++) {
        result += Math.sqrt(Math.cos(Math.sin(base))) + base * base + base + base * 2 + base * 3
      }
      return result
    },
  },
}

想要更詳細瞭解這個問題,可以看這裡:https://mp.weixin.qq.com/s/wu...

v-show 重用 DOM

v-show 固然可以加快元件顯示速度,但是 v-showv-if 的平衡也要掌握好。v-if 可以用於首屏載入速度優化。

分片

  • 分批賦值響應式資料,從而減少每次渲染的時間,提高使用者感知到的流暢度。
  • 重型元件使用 v-if 延後展示
  • 可以藉助 requestAnimationFrame

相關傳送門:requestAnimationFrame

PS:個人體驗,如果多個 ajax 牽扯到相同的一堆資料,分片渲染的速度恐怕並不會快,我會選擇用 Promise.all 合併渲染

總結

元件角度優化:

  • 拆分元件,利用元件級別的更新粒度優化更新速度
  • 慎用重型元件,有必要時自己造,外掛同理
  • 使用函式式元件(低優先)

處理響應式的副作用:

  • 利用響應式的反模式
  • 減少在依賴收集時使用 this

降低渲染壓力:

  • v-showv-if 的平衡
  • 分片渲染

Vue 自帶的快取:

  • keepalive
  • computed

其他優化:

  • 資料快取
  • 虛擬滾動
  • 去除 console.log
  • 啟用 performance 配置

推薦文章

相關文章