Vue 實用開發技巧(性能)


1. 長列表性能優化
在2.x版本中Vue會通過Object.defineProperty對數據進行劫持, 以實現雙向數據綁定.
但在一些特定的業務場景, 組件只需要進行純數據展示, 不會有任何變化, 此時我們可能不需要Vue對來數據進行劫持.
在大量數據需要進行呈現時, 如果禁止Vue對數據進行劫持, 會明顯減少組件初始化的時間.
::: tip
通過Object.freeze方法凍結對象, 對象一旦被凍結就不能再被修改了.
:::
export default {
  data: () => ({
    userList: []
  }),
  async created() {
    const userList = await this.$service.get("/getuserList");
    this.userList = Object.freeze(userList);
  }
};
復制代碼2. Vue組件渲染性能分析
基於上面的案例(長列表性能優化), 可以通過Object.freeze來實現純呈現的列表性能優化, 那如何來確認呢?
我們可以通過Chrome Devtools來檢測. 但為了獲得准確的性能分析數據, 我們需要開啟Vue應用的性能模式.
開啟Vue性能模式(適用於開發模式)
在工程中的main.js中(Vue根實例初始化之前), 添加以下代碼:
Vue.config.performance = true;
復制代碼當然, 你也可以根據需要對當前環境進行判斷, 來決定是否開啟性能模式.
const isDev = process.env.NODE_ENV !== "production";
Vue.config.performance = isDev;
復制代碼這樣, 將會激活Vue在內部用於標記組件性能的 Timing API. 如下圖所示:

假設, 此時我們創建好了一個demo工程, 並有一個Hello.vue的組件, 用於驗證長列表渲染性能問題. 運行本地工程后, 打開瀏覽器到指定路由(確認有加載Hello.vue組件). 打開控制台, 並點擊"reload"按鈕, 如下圖所示:

此時, 將會記錄頁面性能. 因為已經在main.js上添加了Vue.config.performance設置,此時你將能夠在分析中看到時序部分. 如下圖所示.

此時, 你會發現這里有3個指標:

init, 創建組件實例所花費的時間
render, 創建vDOM結構所花費的時間
patch, 將vDOM結構渲染成實際的DOM元素所花費的時間

驗證性能
在此例中, http://localhost:8080/#/hello 路由下, 只有兩個組件:
App.vue
  Hello.vue
復制代碼App.vue是視圖組件, 只有一個<router-view/>
Hello.vue只做一個簡單的長列表(100000條件數據)展示, 代碼如下:
<template>
 <div>
   <span v-for="(item, idx) in users" :key="idx">
     {{item.name}}
   </span>
 </div>
</template>

<script>
export default {
  data () {
    return {
      users: []
    }
  },
  components: {

  },
  created () {
    let users = Array.from({ length: 100000 }, (item, index) => ({ name: index }))
    this.users = users
  }
}
</script>
復制代碼此時, Hello.vue組件render&patch的時間為:

render -> 924ms
patch  -> 1440ms


修改Hello.vue的created鈎子函數中的代碼如下:
created () {
  let users = Array.from({ length: 100000 }, (item, index) => ({ name: index }))
  this.users = Object.freeze(users)
}
復制代碼再次點擊"reload"按鈕, 重新測試性能.

此時, Hello.vue組件render&patch的時間為:

render -> 397ms (上一次測試結果為: 924ms, 節省時間: 527ms, 性能提供約為 57%)
patch  -> 782ms (上一次測試結果為: 1440ms, 節省時間: 658ms, 性能提供約為: 45.7%)

這里僅測試了一次, 但從結果來看, 增加Object.freeze凍結后, 整體性能會有明顯提升.
3. 不使用Vuex創建Store(Vue.observable)

2.6.0 新增


參數:{Object} object
用法:讓一個對象可響應。Vue 內部會用它來處理 data 函數返回的對象。

返回的對象可以直接用於渲染函數和計算屬性內,並且會在發生改變時觸發相應的更新。也可以作為最小化的跨組件狀態存儲器,用於簡單的場景:
const state = Vue.observable({ count: 0 })

const Demo = {
  render(h) {
    return h('button', {
      on: { click: () => { state.count++ }}
    }, `count is: ${state.count}`)
  }
}
復制代碼我們可以利用這個API來應對一些簡單的跨組件數據狀態共享的情況.
// miniStore.js

import Vue from "vue";
 
export const miniStore = Vue.observable({ count: 0 });
 
export const actions = {
  setCount(count) {
    miniStore.count = count;
  }
}

export const getters = {
  count: () => miniStore.count
}

復制代碼// Demo.vue
<template>
  <div>
    <p>count:{{count}}</p>
    <button @click="add"> +1 </button>
    <button @click="sub"> -1 </button>
  </div>
</template>
 
<script>
import { actions, getters } from "./store";
export default {
  name: "App",
  computed: {
    count() {
      return getters.count;
    }
  },
  methods: {
    add: actions.setCount(this.count+1),
    sub: actions.setCount(this.count-1)
  }
};
</script>
 
復制代碼4. 屬性&事件傳遞
在寫Vue組件時, 經常會遇到:

組件層層傳遞props或listerers
動態綁定props或listerers

有沒有什么辦法可以解決以上兩種場景的問題呢?
::: tip
v-bind和v-on, 可以實現解決上述問題
:::
代碼示例如下:
<template>
  <Child v-bind="$props" v-on="$listeners"> </Child>
</template>
 
<script>
  import Child from "./Child";
  export default {
    props: {
      title: {
        required: true,
        type: String
      }
    }
    components: {
      Child
    }
  };
</script>
復制代碼5. 監聽函數的生命周期函數
有時, 需要在父組件監聽子組件掛載后mounted, 做一些邏輯處理.
例如:
加載遠端組件時, 想抓取組件從遠端加載到掛載的耗時.
此時, 就不能用常規的寫法, 在每個子組件中去this.$emit事件了.
有沒有辦法, 只需要在父組件中監聽各子組件的生命周期鈎子函數呢?
::: tip
@hook可以監聽到子組件的生命周期鈎子函數(created, updated等等).
例如: @hook:mounted="doSomething"
:::
// Parent.vue
<template>
  <Child v-bind="$props" v-on="$listeners" @hook:mounted="doSomething"> </Child>
</template>
 
<script>
  import Child from "./Child";
  export default {
    props: {
      title: {
        required: true,
        type: String
      }
    }
    components: {
      Child
    },
    methods: {
      doSomething(){
        console.log("child component has mounted!");
      }
    }
  };
</script>
復制代碼6. 函數式組件
::: tip
函數式組件, 無狀態,無法實例化,內部沒有任何生命周期處理方法,非常輕量,因而渲染性能高,特別適合用來只依賴外部數據傳遞而變化的組件。
:::
寫法如下:

在template標簽里面標明functional
只接受props值
不需要script標簽

<!-- App.vue -->
<template>
  <div>
    <UserList :users="users" :click-handler="clickHandler.bind(this)"></UserList>
  </div>
</template>
 
<script>
import UserList from "./UserList";
 
export default {
  name: "App",
  data: () => {
    users: ['james', 'ian']
  }
  components: { UserList },
  methods: {
    clickHandler(name){
      console.log(`clicked: ${name}`);
    }    
  }
};
</script>
復制代碼// UserList.vue
<template functional>
  <div>
    <p v-for="(name, idx) in props.users" @click="props.clickHandler(name)" :key="idx">
      {{ name }}
    </p>
  </div>
</template>
復制代碼7. 作用域插槽

在 2.6.0 中,Vue為具名插槽和作用域插槽引入了一個新的統一的語法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 這兩個目前已被廢棄但未被移除且仍在文檔中的特性。新語法的由來可查閱這份 RFC。

簡單示例
如何使用作用域插槽呢? 請先看如下示例:
<template>
  <List :items="items">
    <template slot-scope="{ filteredItems }">
      <p v-for="item in filteredItems" :key="item">{{ item }}</p>
    </template>
  </List>
</template>
復制代碼使用v-slot, 可以直接在組件標簽上寫入該插槽的scope.
<template>
  <List v-slot="{ filteredItems }" :items="items">
    <p v-for="item in filteredItems" :key="item">{{ item }}</p>
  </List>
</template>
復制代碼::: tip
v-slot只能在組件或template標簽上使用, 不能使用在普通原生的HTML標簽上.
:::
這樣使得代碼可讀性增強, 特別是在一些很難說明模板變量來源的場景中.
v-slot 高級使用
v-slot指令還引入了一種方法來組合使用slot&scoped-slot, 但需要用":"來分隔.
<template>
  <Promised :promise="usersPromise">
    <p slot="pending">Loading...</p>

    <ul slot-scope="users">
      <li v-for="user in users">{{ user.name }}</li>
    </ul>

    <p slot="rejected" slot-scope="error">Error: {{ error.message }}</p>
  </Promised>
</template>
復制代碼使用v-slot重寫:
<template>
  <Promised :promise="usersPromise">
    <template v-slot:pending>
      <p>Loading...</p>
    </template>

    <template v-slot="users">
      <ul>
        <li v-for="user in users">{{ user.name }}</li>
      </ul>
    </template>

    <template v-slot:rejected="error">
      <p>Error: {{ error.message }}</p>
    </template>
  </Promised>
</template>
復制代碼v-slot還可以簡寫為 # , 重寫上面的例子:
<template>
  <Promised :promise="usersPromise">
    <template #pending>
      <p>Loading...</p>
    </template>

    <template #default="users">
      <ul>
        <li v-for="user in users">{{ user.name }}</li>
      </ul>
    </template>

    <template #rejected="error">
      <p>Error: {{ error.message }}</p>
    </template>
  </Promised>
</template>
復制代碼::: tip
注意, v-slot的簡寫是 #default
:::
8. watch
雖然Vue.js為我們提供了有用的computed, 但在某些場景下, 仍然還是需要使用到watch.
::: tip
默認情況下, watch只在被監聽的屬性值發生變化時執行.
:::
例如:
export default {
  data: () => ({
    dog: ""
  }),
  watch: {
    dog(newVal, oldVal) {
      console.log(`Dog changed: ${newVal}`);
    }
  }
};
復制代碼如上代碼所示, 只有當dog的值有發生改變時, watch中的dog函數才會執行.
但是, 在某些情況下, 你可能需要在創建組件后立即運行監聽程序.
當然, 你可以將邏輯遷移至methods中, 然后從watch和created鈎子函數中分別調用它, 但有沒有更簡單一點的辦法呢?
你可以在使用watch時, 使用immediate: true選項, 這樣它就會在組件創建時立即執行.
export default {
  data: () => ({
    dog: ""
  }),
  watch: {
    dog: {
      handler(newVal, oldVal) {
        console.log(`Dog changed: ${newVal}`);
      },
      immediate: true
    }
  }
};
復制代碼9. 圖片懶加載
v-lazy-image圖片懶加載組件.
安裝:
npm install v-lazy-image
使用:
// main.js
import Vue from "vue";
import { VLazyImagePlugin } from "v-lazy-image";

Vue.use(VLazyImagePlugin);
復制代碼<template>
  <v-lazy-image src="http://lorempixel.com/400/200/" />
</template>
復制代碼你也可以使用漸進式圖像加載方式來加載圖片, 通過設置src-placeholder先加載縮略圖, 同時使用CSS應用自己的過濾效果.
<template>
  <v-lazy-image
    src="http://demo.com/demo.jpeg"
    src-placeholder="http://demo.com/min-demo.jpeg"
  />
</template>

<style scoped>
  .v-lazy-image {
    filter: blur(10px);
    transition: filter 0.7s;
  }
  .v-lazy-image-loaded {
    filter: blur(0);
  }
</style>
復制代碼10. .sync 修飾符

2.3.0+ 新增

在有些情況下,我們可能需要對一個 prop 進行“雙向綁定”。
不幸的是,真正的雙向綁定會帶來維護上的問題,因為子組件可以修改父組件,且在父組件和子組件都沒有明顯的改動來源。
這也是為什么我們推薦以 update:myPropName 的模式觸發事件取而代之。
舉個例子,在一個包含 title的 prop屬性的組件中,我們可以用以下方法表達對其賦新值的意圖:
this.$emit('update:title', newTitle)
復制代碼然后父組件可以監聽那個事件並根據需要更新一個本地的數據屬性。例如:
<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>
復制代碼為了方便起見,我們為這種模式提供一個縮寫,即 .sync 修飾符:
<text-document v-bind:title.sync="doc.title"></text-document>
復制代碼::: danger
帶有 .sync 修飾符的 v-bind 不能和表達式一起使用.
例如:
v-bind:title.sync=”doc.title + ‘!’” 是無效的。
取而代之的是,你只能提供你想要綁定的屬性名,類似 v-model。
:::
當我們用一個對象同時設置多個 prop 的時候,也可以將這個 .sync 修飾符和 v-bind 配合使用:
<text-document v-bind.sync="doc"></text-document>
復制代碼這樣會把 doc 對象中的每一個屬性 (如 title) 都作為一個獨立的 prop 傳進去,然后各自添加用於更新的 v-on 監聽器。
注意
將 v-bind.sync 用在一個字面量的對象上.
例如:
v-bind.sync=”{ title: doc.title }”,是無法正常工作的.
因為在解析一個像這樣的復雜表達式的時候,有很多邊緣情況需要考慮。
11. 調試 Vue template
在Vue開發過程中, 經常會遇到template模板渲染時JavaScript變量出錯的問題, 此時也許你會通過console.log來進行調試. 例如:
<template>
  <h1>
    {{ log(message) }}
  </h1>
</template>
<script>
methods: {
  log(message) {
    console.log(message);
  }
}
</script>
復制代碼每次調試模板渲染時, 都類似重復這樣寫, 可能會很無聊, 有沒有更好的辦法呢?
在Vue.prototype原型鏈上添加一個自定義的方法.
// main.js
Vue.prototype.$log = window.console.log;
復制代碼至止, 我們可以在每個組件的模板中使用$log, 如果我們不想影響模板的渲染, 也可以:
<h1>
  {{ log(message) || message }}
</h1>
復制代碼這樣是不是很方便的調試模板了?
那延展一下, 有沒有辦法增加一個斷點, 以調試模板渲染時, 查看相關聯的變量?
我們在使用模板時放入一個debugger.
<h1>
  {{ debugger }}
</h1>
復制代碼你會發現, 組件根本就沒有編譯模板. 有沒有辦法呢?
我們可以嘗試在模板中添加一個自執行的函數, 例如:
<h1>
  {{ (function(){degugger;}) || message }}
</h1>
復制代碼此時, 我們將可以看到斷點定位到了模板的渲染函數中了.

此時的_vm, 就是我們組件的實例對象.
檢查編譯的模板雖然很有意思, 但由於某些原因, 變量在被我們放在debugger后, 在chrome devtools的函數范圍內變得不可用.
修改下寫法:
<h1>
  {{ (function(){degugger; message}) || message }}
</h1>
復制代碼此時, 你就可以隨心所欲了.

12. Vue組件局部樣式 scoped
Vue中style標簽的scoped屬性表示它的樣式只作用於當前模塊,是樣式私有化, 設計的初衷就是讓樣式變得不可修改.
渲染的規則/原理:

給HTML的DOM節點添加一個 不重復的data屬性 來表示 唯一性
在對應的 CSS選擇器 末尾添加一個當前組件的 data屬性選擇器來私有化樣式,如:.demo[data-v-2311c06a]{}
若組件內部包含其他組件,只會給其他組件的最外層標簽加上當前組件的 data-v 屬性

例如, 如下代碼所示:
<template>
  <div class="demo">
    <span class="content">
      Vue.js scoped
    </span>
  </div>
</template>

<style lang="less" scoped>
  .demo{
    font-size: 14px;
    .content{
      color: red;
    }
  }
</style>

復制代碼瀏覽器渲染后的代碼:
<div data-v-fed36922>
  Vue.js scoped
</div>
<style type="text/css">
.demo[data-v-039c5b43] {
  font-size: 14px;
}
.demo .content[data-v-039c5b43] {
  color: red;
}
</style>
復制代碼::: tip 注意
添加scoped屬性后, 父組件無法修改子組件的樣式.
:::
13. Vue組件樣式之 deep選擇器
如上例中, 若想在父組件中修改子組件的樣式, 怎么辦呢?

1.采用全局屬性和局部屬性混合的方式
2.每個組件在最外層添加一個唯一的class區分不同的組件
3.使用深層選擇器deep

這里我們主要講解使用deep修改子組件的樣式. 將上例的代碼修改為:
<template>
  <div class="demo">
    <span class="content">
      Vue.js scoped
    </span>
  </div>
</template>

<style lang="less" scoped>
  .demo{
    font-size: 14px;
  }
  .demo /deep/ .content{
    color: blue;
  }
</style>

復制代碼最終style編譯后的輸出為:
<style type="text/css">
.demo[data-v-039c5b43] {
  font-size: 14px;
}
.demo[data-v-039c5b43] .content {
  color: blue;
}
</style>
復制代碼從編譯可以看出, 就是.content后有無添加CSS屬性data-v-xxx的區別, 屬性CSS選擇器權重問題的同學, 對此應該立即明白了吧!
14. Vue組件局部樣式 Modules
CSS Modules 是一個流行的,用於模塊化和組合 CSS 的系統。vue-loader 提供了與 CSS Modules 的一流集成,可以作為模擬 scoped CSS 的替代方案。
用法
首先,CSS Modules 必須通過向 css-loader 傳入 modules: true 來開啟:
// webpack.config.js
{
  module: {
    rules: [
      // ... 其它規則省略
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          {
            loader: 'css-loader',
            options: {
              // 開啟 CSS Modules
              modules: true,
              // 自定義生成的類名
              localIdentName: '[local]_[hash:base64:8]'
            }
          }
        ]
      }
    ]
  }
}
復制代碼然后在你的 <style> 上添加 module 特性:
<style module>
.red {
  color: red;
}
.bold {
  font-weight: bold;
}
</style>
復制代碼這個 module 特性指引 Vue Loader 作為名為 $style 的計算屬性,向組件注入 CSS Modules 局部對象。然后你就可以在模板中通過一個動態類綁定來使用它了:
<template>
  <p :class="$style.red">
    This should be red
  </p>
</template>
復制代碼因為這是一個計算屬性,所以它也支持 :class 的對象/數組語法:
<template>
  <div>
    <p :class="{ [$style.red]: isRed }">
      Am I red?
    </p>
    <p :class="[$style.red, $style.bold]">
      Red and bold
    </p>
  </div>
</template>
復制代碼你也可以通過 JavaScript 訪問到它:
<script>
export default {
  created () {
    console.log(this.$style.red)
    // -> "red_1VyoJ-uZ"
    // 一個基於文件名和類名生成的標識符
  }
}
</script>
復制代碼你可以查閱 CSS Modules 規范了解更多細節,諸如 global exceptions 和 composition 等。
可選用法
如果你只想在某些 Vue 組件中使用 CSS Modules,你可以使用 oneOf 規則並在 resourceQuery 字符串中檢查 module 字符串:
// webpack.config.js -> module.rules
{
  test: /\.css$/,
  oneOf: [
    // 這里匹配 `<style module>`
    {
      resourceQuery: /module/,
      use: [
        'vue-style-loader',
        {
          loader: 'css-loader',
          options: {
            modules: true,
            localIdentName: '[local]_[hash:base64:5]'
          }
        }
      ]
    },
    // 這里匹配普通的 `<style>` 或 `<style scoped>`
    {
      use: [
        'vue-style-loader',
        'css-loader'
      ]
    }
  ]
}
復制代碼和預處理器配合使用
CSS Modules 可以與其它預處理器一起使用:
// webpack.config.js -> module.rules
{
  test: /\.scss$/,
  use: [
    'vue-style-loader',
    {
      loader: 'css-loader',
      options: { modules: true }
    },
    'sass-loader'
  ]
}
復制代碼自定義的注入名稱
在 .vue 中你可以定義不止一個 <style>,為了避免被覆蓋,你可以通過設置 module 屬性來為它們定義注入后計算屬性的名稱。
<style module="a">
  /* 注入標識符 a */
</style>

<style module="b">
  /* 注入標識符 b */
</style>

作者:James Zhang
鏈接:https://juejin.im/post/5d790819e51d453b5e465bc7
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM