1. 程式人生 > 其它 >vue自定義指令——v-eharts自定義指令實現實踐

vue自定義指令——v-eharts自定義指令實現實踐

技術標籤:vuevuejs

vue自定義指令

官方文件

作用

自定義指令為什麼存在?官方文件給出的原因如下

…在 Vue2.0 中,程式碼複用和抽象的主要形式是元件。然而,有的情況下,你仍然需要對普通 DOM 元素進行底層操作,這時候就會用到自定義指令。…

對普通dom元素進行底層操作!有點振聾發聵的意思,我們所熟知的vue指令包含了v-if,v-show,v-for,v-model,v-bind,v-on等等,如果再往下想一層,這些指令無一不是在幫我們操作底層的dom。

  • v-if,刪除/新增dom節點

  • v-show,給dom元素新增/刪除內聯樣式style="display: none"

  • v-for,迴圈新增dom節點

  • v-model,更新dom節點資料(除此之外,非dom相關的,操作檢視更新繫結資料)

  • v-bind,更新dom節點資料

  • v-on,給dom元素新增事件監聽

所以指令的使用場景也很明確了,當你需要操作dom的時候,不妨想想能不能使用自定義指令

寫到這裡的時候,腦海中立馬想到一個場景,echarts初始化!那麼繼續探索一下自定義指令的使用來看看是否能通過自定義指令來更為優雅的實現echarts初始化

註冊方式

  • 全域性註冊
// 註冊一個全域性自定義指令 `v-focus`
Vue.directive('focus', {
  // 當被繫結的元素插入到 DOM 中時……
inserted: function (el) { // 聚焦元素 el.focus() } })
  • 元件中區域性註冊
export default {
  name: '',
  data(){
    return {
    }
  },
  ...
  directives: {
    focus: {
      // 指令的定義
      inserted: function (el) {
        el.focus()
      }
    }
	}
  ...
}

然後你可以在模板中任何元素上使用新的 v-focus property,如下:

<input v-focus/>

需要注意的是,在註冊自定義指令時,指令名稱並不需要寫v-

如果在區域性和全域性都設定了相同的指令,會以區域性設定為準

鉤子函式

如何告知瀏覽器我們希望何時希望呼叫我們通過指令實現的方法,那就是通過鉤子函式,自定義指令目前提供了以下五種鉤子函式

  • bind:只調用一次,指令第一次繫結到元素時呼叫。在這裡可以進行一次性的初始化設定。
  • inserted:被繫結元素插入父節點時呼叫 (僅保證父節點存在,但不一定已被插入文件中)。
  • update:所在元件的 VNode 更新時呼叫,但是可能發生在其子 VNode 更新之前。指令的值可能發生了改變,也可能沒有。但是你可以通過比較更新前後的值來忽略不必要的模板更新 (詳細的鉤子函式引數見下)。
  • binding的value值發生變化時,會觸發
  • componentUpdated:指令所在元件的 VNode 及其子 VNode 全部更新後呼叫。
  • unbind:只調用一次,指令與元素解綁時呼叫。
    • 所在元件被銷燬時,會被呼叫

鉤子函式引數

在瞭解了鉤子函式之後,再來了解一下鉤子函式的引數

指令鉤子函式會被傳入以下引數:

  • el:指令所繫結的元素,可以用來直接操作 DOM。

  • binding:一個物件,包含以下 property:

  • name:指令名,不包括 v- 字首。

    • value:指令的繫結值,例如:v-my-directive="1 + 1" 中,繫結值為 2
  • oldValue:指令繫結的前一個值,僅在 updatecomponentUpdated 鉤子中可用。無論值是否改變都可用。

    • expression:字串形式的指令表示式。例如 v-my-directive="1 + 1" 中,表示式為 "1 + 1"
    • arg:傳給指令的引數,可選。例如 v-my-directive:foo 中,引數為 "foo"
    • modifiers:一個包含修飾符的物件。例如:v-my-directive.foo.bar 中,修飾符物件為 { foo: true, bar: true }
  • vnode:Vue 編譯生成的虛擬節點。移步 VNode API 來了解更多詳情。

  • oldVnode:上一個虛擬節點,僅在 updatecomponentUpdated 鉤子中可用。

以下面的程式碼為例,

<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>
Vue.directive('demo', {
  bind: function (el, binding, vnode) {
    var s = JSON.stringify
    el.innerHTML =
      'name: '       + s(binding.name) + '<br>' +
      'value: '      + s(binding.value) + '<br>' +
      'expression: ' + s(binding.expression) + '<br>' +
      'argument: '   + s(binding.arg) + '<br>' +
      'modifiers: '  + s(binding.modifiers) + '<br>' +
      'vnode keys: ' + Object.keys(vnode).join(', ')
  }
})

new Vue({
  el: '#hook-arguments-example',
  data: {
    message: 'hello!'
  }
})

頁面上展示的結果為:

在這裡插入圖片描述

vue自定義指令實現echarts初始化

在vue中引用echarts,panjiachen的vue-element-admin已經給出了最佳實踐,一個圖表封裝成一個元件,並通過mixin混入視窗大小重置時的圖表resize事件。

那麼如果想通過自定義指令來實現echarts的初始化,有以下幾點需要注意

鉤子函式的選擇

  • bind首先排除,bind的觸發時機早於dom插入文件,這個時候連ehcarts例項都無法有效的初始化,無法有效的渲染圖表

  • 然後是inserted,這個時候滿足了dom插入的條件,如果是通過靜態資料來渲染圖表,在inserted中足以實現echarts的渲染,實際專案中,圖表資料往往是通過後端非同步獲取的,根據瀏覽器的事件迴圈機制,這時拿到的option可能是沒有資料的,或者根本沒有option,這和具體的程式碼實現有關

  • 最優選擇是update,當指令傳入的binding值發生變化時,會觸發,那麼將option作為binding的內容傳入,當option發生變化時,開始ehars的初始化即可

程式碼如下:

import Vue from 'vue'
// echarts5只能使用require引入?
const echarts = require('echarts')
Vue.directive('echarts', {
  inserted: (el, binding, vnode) => {
    if (binding.value) {
      const myChart = echarts.init(el)
      myChart.setOption(binding.value)
    }
  },
  update: (el, binding, vnode) => {
    if (binding.value) {
      const myChart = echarts.init(el)
      myChart.setOption(binding.value)
    }
  }
})

在模板中使用

<template>
  <div>
    <div class="echarts-demo" v-echarts="option"></div>
  </div>
</template>

ehcarts resize解決方案

通過vnode引數操作vue例項屬性

可以看到,上述的實現中,不同於一般的在vue中初始化例項,ehchats的例項在鉤子函式中生成,也就是vue例項中沒有儲存ehcarts例項的引用,但是想在使用了v-echarts的vue元件中掛載生成的echarts例項也不是不可以,鉤子函式的第三個引數vnode傳入了指令所在元素的虛擬dom,該物件上有一個屬性context,可以獲取使用自定義指令所在的vue component,也就是可以使用下面這種方式

import Vue from 'vue'
// echarts5只能使用require引入?
const echarts = require('echarts')
Vue.directive('echarts', {
  inserted: (el, binding, vnode) => {
    if (binding.value) {
      const myChart = echarts.init(el)
      myChart.setOption(binding.value)
      const context = vnode.context
      context.chart = myChart
    }
  },
  update: (el, binding, vnode) => {
    if (binding.value) {
      const myChart = echarts.init(el)
      myChart.setOption(binding.value)
      const context = vnode.context
      context.chart = myChart
    }
  }
})

在myEchartsComponent.vue中可以這樣使用

<template>
  <div>
    <div class="echarts-demo" v-echarts="option"></div>
  </div>
</template>

<script>
export default {
  name: 'Ldirective',
  created () {
    setTimeout(() => {
      this.option = {
        title: {
          text: '第一個 ECharts 例項'
        },
        tooltip: {},
        legend: {
          data: ['銷量']
        },
        xAxis: {
          data: ['襯衫', '羊毛衫', '雪紡衫', '褲子', '高跟鞋', '襪子']
        },
        yAxis: {},
        series: [{
          name: '銷量',
          type: 'bar',
          data: [5, 20, 36, 10, 10, 20]
        }]
      }
    }, 100)
  },
  data () {
    return {
      option: null,
      chart: null
    }
  }
}
</script>

<style scoped lang="less">
.echarts-demo{
  width: 100%;
  height: 500px;
}
</style>

但是這種感覺會非常奇怪,因為在data中加入一個屬性只是為了在指令執行的時候需要使用,開發者並不顯式的在vue例項中操作該屬性,然後通過在元件中加入混入的方式,來實現視窗resize時圖示resize,那使用指令就顯得極其雞肋了,和vue-element-admin專案沒有太大的區別。指令最方便的使用應該是隻提供指令所需的資料,它應該讓開發者關注指令在使用時所傳入的表示式,而不用去理會後面的處理邏輯,這種做法導致了開發者在使用這個指令時,必須在data返回的物件中加入charts屬性,可能文件裡面會說“該屬性用於儲存該元件中初始化的ehcarts例項,你可能會需要用到它”,如果是我只會覺得這個指令真的垃圾,所以這樣的解決方案是不能接受的。

在指令中完成事件繫結

首先需要實現一個ResizeHandler類

class ResizeHandler {
  constructor (chart) {
    this.$_resizeHandler = null
    this.chart = chart
  }

  initListener () {
    // 這裡的debounce是防抖函式的封裝
    this.$_resizeHandler = debounce(() => {
      this.resize()
    }, 1000)
    window.addEventListener('resize', this.$_resizeHandler)
  }

  destroyListener () {
    window.removeEventListener('resize', this.$_resizeHandler)
    this.chart.dispose()
    this.chart = null
  }

  resize () {
    const { chart } = this
    chart && chart.resize()
  }
}

建構函式接收一個ehcarts例項物件

下面來重寫v-echarts指令

Vue.directive('echarts', {
  inserted: (el, binding, vnode) => {
    if (binding.value.option) {
      // 建立新的resizeHandler操作類
      const myChart = echarts.init(el)
      myChart.setOption(binding.value.option)
      const resizeHandler = new ResizeHandler(myChart)
      resizeHandler.initListener()
      vnode.context[`$${binding.value.id}ResizeHandler`] = resizeHandler
    }
  },
  update: (el, binding, vnode) => {
    // 已經初始化過,當option更新時只需要重新呼叫setOption方法初始化圖表
    if (vnode.context[`$${binding.value.id}ResizeHandler`]) {
      if (binding.value.option) {
        const resizeHandler = vnode.context[`$${binding.value.id}ResizeHandler`]
        resizeHandler.chart.setOption(binding.value.option)
      }
    } else if (binding.value.option) {
      // 非首次初始化,建立新的resizeHandler操作類
      const myChart = echarts.init(el)
      myChart.setOption(binding.value.option)
      const resizeHandler = new ResizeHandler(myChart)
      resizeHandler.initListener()
      vnode.context[`$${binding.value.id}ResizeHandler`] = resizeHandler
    }
  },
  unbind: (el, binding, vnode) => {
    vnode.context[`$${binding.value.id}ResizeHandler`] && vnode.context[`$${binding.value.id}ResizeHandler`].destroyListener()
  }
})

改變了binding傳入的引數型別

使用方法如下:

<template>
  <div>
    <div class="echarts-demo" v-echarts="{option: option, id: 'first'}"></div>
    <div class="echarts-demo" v-echarts="{option: option1, id: 'second'}"></div>
  </div>
</template>

可以看到這裡將指令傳入的值由原來的option修改為了一個帶id的物件,帶id是為了防止當一個vue元件需要多個ehcarts圖表時,在vue指令中可以通過在元件例項中繫結不同的屬性名,並且不用在元件的data方法中顯式地宣告,最重要的是,在unbind鉤子函式執行(元件銷燬)時,能夠知道銷燬的是哪一個ehcarts的resizeHandler例項物件,那麼到這個地方,echarts的vue指令實現版已經完成,可以發現,相比vue-element-admin專案,將resize事件和echarts銷燬全部放在了指令中實現,只需要修改option就可以實現echarts的初始化