Vue 實用開發技巧

1. 長列表性能優化

在2.x版本中Vue會經過Object.defineProperty對數據進行劫持, 以實現雙向數據綁定. 但在一些特定的業務場景, 組件只須要進行純數據展現, 不會有任何變化, 此時咱們可能不須要Vue對來數據進行劫持. 在大量數據須要進行呈現時, 若是禁止Vue對數據進行劫持, 會明顯減小組件初始化的時間.javascript

::: tip 經過Object.freeze方法凍結對象, 對象一旦被凍結就不能再被修改了. :::css

export default {
  data: () => ({
    userList: []
  }),
  async created() {
    const userList = await this.$service.get("/getuserList");
    this.userList = Object.freeze(userList);
  }
};
複製代碼

2. Vue組件渲染性能分析

基於上面的案例(長列表性能優化), 能夠經過Object.freeze來實現純呈現的列表性能優化, 那如何來確認呢?html

咱們能夠經過Chrome Devtools來檢測. 但爲了得到準確的性能分析數據, 咱們須要開啓Vue應用的性能模式.vue

開啓Vue性能模式(適用於開發模式)

在工程中的main.js中(Vue根實例初始化以前), 添加如下代碼:java

Vue.config.performance = true;
複製代碼

固然, 你也能夠根據須要對當前環境進行判斷, 來決定是否開啓性能模式.webpack

const isDev = process.env.NODE_ENV !== "production";
Vue.config.performance = isDev;
複製代碼

這樣, 將會激活Vue在內部用於標記組件性能的 Timing API. 以下圖所示: git

images.png

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

images.png

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

images.png

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

  • 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

images.png

修改Hello.vuecreated鉤子函數中的代碼以下:

created () {
  let users = Array.from({ length: 100000 }, (item, index) => ({ name: index }))
  this.users = Object.freeze(users)
}
複製代碼

再次點擊"reload"按鈕, 從新測試性能.

images.png

此時, 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組件時, 常常會遇到:

  • 組件層層傳遞propslisterers
  • 動態綁定propslisterers

有沒有什麼辦法能夠解決以上兩種場景的問題呢?

::: tip v-bindv-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中, 而後從watchcreated鉤子函數中分別調用它, 但有沒有更簡單一點的辦法呢?

你能夠在使用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. provide / inject

2.2.0 新增

類型:

  • provide:Object | () => Object
  • inject:Array | { [key: string]: string | Symbol | Object }

注意: provide 和 inject 主要爲高階插件/組件庫提供用例。並不推薦直接用於應用程序代碼中。

這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效。若是你熟悉 React,這與 React 的上下文特性很類似。

provide 選項應該是一個對象或返回一個對象的函數。該對象包含可注入其子孫的屬性。在該對象中你可使用 ES2015 Symbols 做爲 key,可是隻在原生支持 Symbol 和 Reflect.ownKeys 的環境下可工做。

inject 選項應該是:

  • 一個字符串數組,或
  • 一個對象,對象的 key 是本地的綁定名,value 是:
    • 在可用的注入內容中搜索用的 key (字符串或 Symbol),或
    • 一個對象,該對象的:
      • from 屬性是在可用的注入內容中搜索用的 key (字符串或 Symbol)
      • default 屬性是降級狀況下使用的 value

提示: provide 和 inject 綁定並非可響應的。這是刻意爲之的。然而,若是你傳入了一個可監聽的對象,那麼其對象的屬性仍是可響應的。

示例:

// 父級組件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子組件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}
複製代碼

利用 ES2015 Symbols、函數 provide 和對象 inject:

const s = Symbol()

const Provider = {
  provide () {
    return {
      [s]: 'foo'
    }
  }
}

const Child = {
  inject: { s },
  // ...
}
複製代碼

在 2.5.0+ 的注入能夠經過設置默認值使其變成可選項:

const Child = {
  inject: {
    foo: { default: 'foo' }
  }
}
複製代碼

若是它須要從一個不一樣名字的屬性注入,則使用 from 來表示其源屬性:

const Child = {
  inject: {
    foo: {
      from: 'bar',
      default: 'foo'
    }
  }
}
複製代碼

與 prop 的默認值相似,你須要對非原始值使用一個工廠方法:

const Child = {
  inject: {
    foo: {
      from: 'bar',
      default: () => [1, 2, 3]
    }
  }
}
複製代碼

12. 調試 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>
複製代碼

此時, 咱們將能夠看到斷點定位到了模板的渲染函數中了.

images.png

此時的_vm, 就是咱們組件的實例對象.

檢查編譯的模板雖然頗有意思, 但因爲某些緣由, 變量在被咱們放在debugger後, 在chrome devtools的函數範圍內變得不可用.

修改下寫法:

<h1>
  {{ (function(){degugger; message}) || message }}
</h1>
複製代碼

此時, 你就能夠爲所欲爲了.

images.png

13. 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屬性後, 父組件沒法修改子組件的樣式. :::

14. 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選擇器權重問題的同窗, 對此應該當即明白了吧!

15. 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 exceptionscomposition 等。

可選用法

若是你只想在某些 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>
複製代碼

相關連接

相關文章
相關標籤/搜索