你應該知道的Vue高級特性

本文使用的Vue版本:2.6.10css

Vue爲咱們提供了不少高級特性,學習和掌握它們有助於提升你的代碼水平。html

1、watch進階

從咱們剛開始學習Vue的時候,對於偵聽屬性,都是簡單地以下面通常使用:vue

watch:{
	a(){
	 //doSomething
	}
}

實際上,Vue對watch提供了不少進階用法。webpack

handler函數

以對象和handler函數的方式來定義一個監聽屬性,handler就是處理監聽變更時的函數:web

watch:{
	a:{
		handler:'doSomething'
	}
},
methods:{
	doSomething(){
		//當 a 發生變化的時候,作些處理
	}
}

handler有啥用?是畫蛇添足麼?用途主要有兩點:正則表達式

  • 將處理邏輯抽象出去了,以method的方式被複用
  • 給定義下面兩個重要屬性留出了編寫位置

deep屬性

不知道你注意到了沒有?數組

當watch的是一個Object類型的數據,若是這個對象內部的某個值發生了改變,並不會觸發watch動做!緩存

也就是說,watch默認狀況下,不監測內部嵌套數據的變更。可是不少狀況下,咱們是須要監測的!服務器

爲解決這一問題,就要使用deep屬性:app

watch:{
	obj:{
		handler:'doSomething',
		deep:true
	}
},
methods:{
	doSomething(){
		//當 obj 發生變化的時候,作些處理
	}
}

deep屬性默認爲false,也就是咱們經常使用的watch模式。

immediate屬性

watchhandler函數一般狀況下只有在監聽的屬性發生改變時纔會觸發。

但有些時候,咱們但願在組件建立後,或者說watch被聲明和綁定的時候,馬上執行一次handler函數,這就須要使用immediate屬性了,它默認爲false,改成true後,就會馬上執行handler。

watch:{
	obj:{
		handler:'doSomething',
		deep:true,
		immediate:true
	}
},
methods:{
	doSomething(){
		//當 obj 發生變化的時候,作些處理
	}
}

同時執行多個方法

使用數組能夠設置多項,形式包括字符串、函數、對象

watch: {
    // 你能夠傳入回調數組,它們會被逐一調用
    a: [
        
      'handle1',
        
      function handle2 (val, oldVal) { /* ... */ },
        
      {
        handler: function handle3 (val, oldVal) { /* ... */ },
        /* ... */
      }
        
    ],
    
  }

2、$event的不一樣表現

$event 是事件對象的特殊變量,在兩種場景下,它有不一樣的意義,表明不一樣的對象。

  • 在原生事件中表示事件自己。能夠經過$event.target得到事件所在的DOM對象,再經過value進一步獲取具體的值。
<template>
    <div>
        <input type="text" @input="inputHandler('hello', $event)" />
    </div>
</template>


export default {
    methods: {
        inputHandler(msg, e) {
            console.log(e.target.value)
        }
    }
}
  • 而在父子組件經過自定義事件進行通訊時,表示從子組件中傳遞出來的參數值

看下面的例子:

//blog-post組件的模板

<button v-on:click="$emit('enlarge-text', 0.1)">
  Enlarge text
</button>

在父級組件監聽這個事件的時候,能夠經過 $event 訪問到blog-post子組件傳遞出來的0.1這個值:

<blog-post
  ...
  v-on:enlarge-text="postFontSize += $event"
></blog-post>

此時,$event的值就是0.1,而不是前面的事件對象。

3、異步更新隊列

  • Vue 在更新 DOM 時是異步執行的。
  • 只要偵聽到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動。
  • 若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。

這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做是很是重要的。而後,在下一個的事件循環「tick」中,Vue 刷新隊列並執行實際 (已去重的) 工做。Vue 在內部對異步隊列嘗試使用原生的 Promise.thenMutationObserversetImmediate,若是執行環境不支持,則會採用 setTimeout(fn, 0) 代替。

例如,當你設置 vm.someData = 'new value',該組件不會當即從新渲染。當刷新隊列時,組件會在下一個事件循環「tick」中更新。

多數狀況咱們不須要關心這個過程,可是若是你想基於更新後的 DOM 狀態來作點什麼,這就可能會有些棘手。

雖然 Vue.js 一般鼓勵開發人員使用「數據驅動」的方式思考,避免直接接觸 DOM,可是有時咱們必需要這麼作。爲了在數據變化以後等待 Vue 完成更新 DOM,能夠在數據變化以後當即使用 Vue.nextTick(callback)

這樣回調函數將在 DOM 更新完成後被調用。例如:

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改數據
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

在組件內使用 vm.$nextTick() 實例方法特別方便,由於它不須要全局 Vue,而且回調函數中的 this 將自動綁定到當前的 Vue 實例上:

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
    }
  }
})

由於 $nextTick() 返回一個 Promise 對象,因此你可使用新的 ES2017 async/await 語法完成相同的事情:

methods: {
  updateMessage: async function () {
    this.message = '已更新'
      //在這裏能夠看出,message並無馬上被執行
      //要理解頁面刷新和代碼執行速度的差異
      //一般咱們在頁面上馬上就能看到結果,那是由於一輪隊列執行其實很快,感受不出DOM刷新的過程和所耗費的時間
      //但對於代碼的執行,屬於即刻級別,DOM沒更新就是沒更新,就是會有問題
    console.log(this.$el.textContent) // => '未更新'
      
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}

通俗的解釋

  • Vue的DOM刷新機制是個異步隊列,並非你想象中的馬上、立刻、即時更新!

  • 這個異步隊列是一輪一輪的執行並刷新

  • 上面帶來的問題是,一些依賴DOM更新完畢才能進行的操做(好比對新增長的DOM元素進行事件綁定),沒法馬上執行,必須等待一輪隊列執行完畢

  • 最容易碰到上面問題的地方:created生命週期鉤子函數中對DOM進行操做

  • 解決辦法:使用this.nextTick(回調函數)方法,將對DOM的操做做爲它的回調函數使用。

4、函數式組件

由於傳統編寫模板的能力不足,咱們引入了渲染函數createElement。咱們又但願得到更多的靈活度,因而引入了JSX。最後,咱們發現有些簡單的模板能夠更簡單更小巧的實現,因而引入了函數式組件。Vue老是試圖爲每一種場景提供不一樣的能力。

有這麼一類組件,它的特色是:

  • 比較簡單
  • 沒有管理任何狀態,也就是說無狀態,沒有響應式數據
  • 沒有監放任何傳遞給它的狀態
  • 沒有寫生命週期方法
  • 本質上只是一個接收一些prop的函數
  • 沒有實例,沒有this上下文

那麼這個組件能夠定義爲函數式組件。與普通組件相比,函數式組件是無狀態的,沒法實例化,沒有任何的生命週期和方法,適合只依賴於外部數據的變化而變化的組件,因其輕量,渲染性能會有所提升。

建立函數式組件

  • 以定義全局組件的方式
Vue.component('my-component', {
  functional: true,
  // Props 是可選的
  props: {
    // ...
  },
  // 爲了彌補缺乏的實例
  // 提供第二個參數做爲上下文
  render: function (createElement, context) {
    // ...
  }
})

注意其中的functional: true,

在 Vue 2.3.0 或以上的版本中,你能夠省略 props 選項,全部組件上的 attribute 都會被自動隱式解析爲 prop。

當使用函數式組件時,該引用將會是 HTMLElement,由於他們是無狀態的也是無實例的。

  • 對於單文件組件,建立函數式組件的方式是在模板標籤內,添加functional屬性
<template functional>
...
</template>

<script>
...
</script>

<style>
...
</style>

最重要的context參數

由於無狀態,沒有this上下文,因此函數式組件須要的一切都是經過 context 參數來傳遞,它是一個包括以下字段的對象:

  • props:提供全部 prop 的對象
  • children:VNode 子節點的數組
  • slots:一個函數,返回了包含全部插槽的對象
  • scopedSlots:(2.6.0+) 一個暴露傳入的做用域插槽的對象。也以函數形式暴露普通插槽。
  • data:傳遞給組件的整個數據對象,做爲 createElement 的第二個參數傳入組件
  • parent:對父組件的引用
  • listeners:(2.3.0+) 一個包含了全部父組件爲當前組件註冊的事件監聽器的對象。這是 data.on 的一個別名。
  • injections:(2.3.0+) 若是使用了 inject 選項,則該對象包含了應當被注入的 property。

應用場景

函數式組件的一個典型應用場景是做爲包裝組件,好比當你碰到下面需求時:

  • 程序化地在多個組件中選擇一個來代爲渲染;
  • 在將 childrenpropsdata 傳遞給子組件以前操做它們。

下面是一個 smart-list 組件的例子,它能根據傳入 prop 的值來代爲渲染更具體的組件:

var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }

Vue.component('smart-list', {
  functional: true,
  props: {
    items: {
      type: Array,
      required: true
    },
    isOrdered: Boolean
  },
  render: function (createElement, context) {
    function appropriateListComponent () {
      var items = context.props.items

      if (items.length === 0)           return EmptyList
      if (typeof items[0] === 'object') return TableList
      if (context.props.isOrdered)      return OrderedList

      return UnorderedList
    }

    return createElement(
      appropriateListComponent(),
      context.data,
      context.children
    )
  }
})

5、監聽子組件的生命週期

假如咱們有父組件Parent和子組件Child,若是在父組件中須要監聽子組件的mounted這個生命週期函數,並作一些邏輯處理,常規寫法可能以下:

// Parent.vue
<Child @mounted="doSth" />

//Child.vue
mounted(){
    this.$emit('mounted');
}

可是,Vue給咱們提供了一種更簡便的方法,子組件無需作任何處理,只須要在父組件引用子組件時使用@hook事件來監聽便可,代碼以下:

// Parent.vue

<Child @hook:mounted="doSth" />  

methods:{
    doSth(){
        //some codes here
    }
}

核心是@hook:mounted="doSth"的寫法!

固然這裏不只僅能夠監聽mounted,其餘生命週期均可以監聽,例如created、updated等。

6、樣式穿透

咱們知道,在單文件組件的style中使用 scoped 屬性後,父組件的樣式將不會滲透到子組件中。

不過一個子組件的根節點會同時受其父組件的 scoped CSS 和子組件的 scoped CSS 的影響。這樣設計是爲了讓父組件能夠從佈局的角度出發,調整其子組件根元素的樣式。

若是你但願父組件的 scoped 樣式中的一個選擇器可以做用得「更深」,例如影響子組件,可使用深度選擇器: >>> 操做符。

<style scoped>
.a >>> .b { /* ... */ }
</style>

上述代碼將會編譯成:

.a[data-v-f3f3eg9] .b { /* ... */ }

可是,有些像 Sass 之類的預處理器沒法正確解析 >>>。這種狀況下你可使用 /deep/::v-deep 操做符,這二者都是 >>> 的別名,實現一樣的功能。

咱們都知道,經過 v-html 建立的 DOM 內容不受 scoped 樣式影響,能夠經過深度做用選擇器>>>來爲他們設置樣式。

7、路由的props屬性

通常在組件內使用路由參數,大多數人會這樣作:

export default {
    methods: {
        getParamsId() {
            return this.$route.params.id
        }
    }
}

當你隨便用用,臨時湊手,這沒什麼問題,畢竟解決了需求。

可咱們要隨時謹記:組件是用來複用的!組件應該有高度的封閉性!

在組件中使用 $route 會使它與路由系統造成高度耦合,從而使組件只能在使用了路由功能的項目內,或某些特定的 URL 上使用,限制了其靈活性。

試想一下,若是你的組件被人拿去複用了,可是那我的並無使用路由系統,而是經過別的方式傳遞id參數,那麼他該怎麼辦?

正確的作法是經過 props 解耦

首先,爲組件定義一個叫作id的prop:

export default {
    props: ['id'],
    methods: {
        getParamsId() {
            return this.id
        }
    }
}

若是組件沒有對應路由,那麼這個id也能夠經過父組件向子組件傳值的方式使用。

若是使用了路由,能夠經過路由的prop屬性,傳遞id的值:

const router = new VueRouter({
    routes: [{
        path: '/user/:id',
        component: User,
        props: true
    }]
})

將路由的 props 屬性設置爲 true 後,組件內可經過 props 接收到 params 參數

另外,你還能夠經過函數模式來返回 props

const router = new VueRouter({
    routes: [{
        path: '/user/:id',
        component: User,
        props: (route) => ({
            id: route.query.id
        })
    }]
})

其實,上面的技巧,在VueRouter的官檔都有說明。

8、異步組件

在大型應用中,咱們可能須要將應用分割成小一些的代碼塊,而且只在須要的時候才從服務器加載一個模塊。

爲了簡化,Vue 容許你以一個工廠函數的方式定義你的組件,這個工廠函數會異步解析你的組件定義。Vue 只有在這個組件須要被渲染的時候纔會觸發該工廠函數,且會把結果緩存起來供將來重渲染。例如:

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回調傳遞組件定義
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})

如你所見,這個工廠函數會收到一個 resolve 回調,這個回調函數會在你從服務器獲得組件定義的時候被調用。

你也能夠調用 reject(reason) 來表示加載失敗。這裏的 setTimeout 是爲了演示用的,如何獲取組件取決於你本身。

一個推薦的作法是將異步組件和 webpack 的 code-splitting 功能一塊兒配合使用:

Vue.component('async-webpack-example', function (resolve) {
  // 這個特殊的 `require` 語法將會告訴 webpack
  // 自動將你的構建代碼切割成多個包,這些包
  // 會經過 Ajax 請求加載
  require(['./my-async-component'], resolve)
})

你也能夠在工廠函數中返回一個 Promise,因此把 webpack 2 和 ES2015 語法加在一塊兒,咱們能夠寫成這樣:

Vue.component(
  'async-webpack-example',
  // 這個 `import` 函數會返回一個 `Promise` 對象。
  () => import('./my-async-component')
)

當使用局部註冊組件的時候,你也能夠直接提供一個返回 Promise 的函數:

new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})

若是你想實現異步加載組件的功能,提升首屏顯示速度,那麼可使用上面例子中的定義組件的方法,也就是:箭頭函數+import語句!

處理加載狀態

2.3.0+ 新增

異步組件的工廠函數也能夠返回一個以下格式的對象,用來靈活定製異步加載過程:

const AsyncComponent = () => ({
  // 須要加載的組件 (應該是一個 `Promise` 對象)
  component: import('./MyComponent.vue'),
  // 異步組件加載時使用的組件
  loading: LoadingComponent,
  // 加載失敗時使用的組件
  error: ErrorComponent,
  // 展現加載時組件的延時時間。默認值是 200 (毫秒)
  delay: 200,
  // 若是提供了超時時間且組件加載也超時了,
  // 則使用加載失敗時使用的組件。默認值是:`Infinity`
  timeout: 3000
})

注意若是你但願在 Vue Router 的路由組件中使用上述語法的話,必須使用 Vue Router 2.4.0+ 版本。

9、批量導入組件

不少時候咱們會編寫一些相似輸入框或按鈕之類的基礎組件,它們是相對通用的組件,稱爲基礎組件,它們會在更大一些的組件中被頻繁的用到。

這很容易致使大的組件裏有一個很長的導入基礎組件的語句列表,例如:

import BaseButton from './BaseButton.vue'
import BaseIcon from './BaseIcon.vue'
import BaseInput from './BaseInput.vue'
//更多導入

export default {
  components: {
    BaseButton,
    BaseIcon,
    BaseInput
  }
}

當你的基礎組件不少的時候,這個過程將很是重複、麻煩和無聊。

require.context()

若是你剛好使用了 webpack (或在內部使用了 webpack 的 Vue CLI 3+),那麼就可使用 require.context 方法批量導入這些組件,而後將它們註冊爲全局組件,這樣就能夠在任何地方直接使用它們了,不再用爲導入的事情煩惱了!

下面是一個示例代碼:

import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

const requireComponent = require.context(
  // 其組件目錄的相對路徑
  './components',
  // 是否查詢其子目錄
  false,
  // 匹配基礎組件文件名的正則表達式
  /Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
  // 獲取組件的配置,也就是具體內容,具體定義,組件的自己代碼
  const componentConfig = requireComponent(fileName)

  // 獲取組件的 PascalCase 命名,用來規範化組件名
  const componentName = upperFirst(
    camelCase(
      // 獲取和目錄深度無關的文件名
      fileName
        .split('/')
        .pop()
        .replace(/\.\w+$/, '')
    )
  )

  // 全局註冊組件
  Vue.component(
    componentName,
    // 若是這個組件選項是經過 `export default` 導出的,
    // 那麼就會優先使用 `.default`,
    // 不然回退到使用模塊的根。
    componentConfig.default || componentConfig
  )
})

記住全局註冊的行爲必須在根 Vue 實例 (經過 new Vue) 建立以前發生

更多內容請訪問: https://www.liujiangblog.com

更多視頻教程請訪問: https://www.liujiangblog.com/video/

相關文章
相關標籤/搜索