# 事件監聽器
目前為止介紹了下面幾種事件監聽的時機:
- 元素上使用 v-on監聽原生事件
- 父組件設定 v-on設定所需要監聽的事件,子組件用$emit觸發事件
- 在 Vue 實體上設定生命週期鉤子監聽各個鉤子事件
上面的方式都是在組件被定義時就要做的設定,也就是靜態的定義方式,但如果要在執行時去動態增減事件的監聽,這時就需要用到 $on, $once 及 $off 這些 JS 函式來做設定。
# 靜態事件監聽
開始說明動態方式之前,先來複習一下靜態的方式。
# 元素上使用 v-on 監聽原生事件
 <div id="app">
    <button @click="alert('native')">native</button>
</div>
var vm = new Vue({
  el: '#app'
});
簡單使用 v-on 註冊原生事件,就可以設定事件的監聽。
# 父組件設定 v-on 設定所需要監聽的事件,子組件用 $emit 觸發事件
 如果要使用自定義事件,在子組件中設定 $emit 發送信號表示欲叫用何種事件,這時父組件如果有使用 v-on 註冊該事件,就會觸發事件。
<div id="app">
  <my-button @component-click="alert('component')">component</my-button>
</div>
Vue.component('my-button', {
  template: `<button @click="$emit('component-click')"><slot></slot></button>`
});
var vm = new Vue({
  el: '#app'
});
在 <my-button> 按下按鈕後,送出的 component-click 信號會被 v-on 所註冊的 component-click 接收並觸發事件。
# 在 Vue 實體上設定生命週期鉤子監聽各個鉤子事件
鉤子事件的觸發可以在 Vue 實體上設定各個 hook 的函式。
var vm = new Vue({
  el: '#app',
  mounted: function() {
    alert('mounted');
  }
});
設定 mounted 函式,就可以監聽 mounted 鉤子。
# 動態註冊事件監聽器
在整個專案都是使用 Vue.js 構築時,使用靜態註冊事件監聽器是足夠的,但如果專案中 Vue.js 只佔了一部分,要配合其他框架或工具設定監聽事件的話,這時就要靠動態註冊事件。
下面的例子模擬在其他非 Vue 元素建立後,要去註冊對應的事件使 Vue 可以觸發。
<div id="app">
  <div id="vue-app">
    <button @click="$emit('on')">Emit</button>
  </div>
  <div id="other-app">
    <button onclick="otherAppClick()">Add Event</button>
    <button onclick="otherAppClick('once')">Add Event ( once )</button>
    <button onclick="otherAppClick('off')">Off All Event</button>
  </div>
</div>
let echoFunc = (echoStr) => () => {alert(echoStr)};
var vm = new Vue({
  el: '#vue-app'
});
function otherAppClick(type) {
  if (type === 'once') {
    vm.$once('on', echoFunc('once'));
  } else if (type === 'off') {
    vm.$off('on');
  } else {
    vm.$on('on', echoFunc('on'));
  }
}
- 在 Vue 實體中的按鈕點擊事件觸發 on事件
- 在 Vue 實體外按鈕點擊中模擬 $on,$once及$off方法
如果沒有使用函式註冊事件的話,在 Vue 實體以外的事件是不能被 $emit 所觸發的。
# 動態註冊鉤子方法
在使用第三方庫(ex: Pikaday)時,最需要注意的就是在 Vue 實體銷毀時也要一併把庫所建立的實體銷毀,以避免 memory leak 的問題。
一般我們會像下面這樣寫:
new Vue({
  el: '#app',
  data: {
    date: null,
    picker: null
  },
  mounted: function () {
    this.picker = new Pikaday({
      field: this.$refs.dateInput,
      format: 'YYYY-MM-DD'
    });
  },
  beforeDestroy: function() {
    this.picker.destroy();
  }
});
- 使用 picker 當作接收 Pikaday 實體的物件
- 於 mounted鉤子中建立 Pikaday 實體
- 在 beforeDestroy時叫用 Pikaday 的destroy方法
這樣做有兩個壞處:
- 需要管理 picker 物件,但這物件只是為了銷毀 Pickaday 實體使用,跟此 Vue 實體沒有關係
- 建立及銷毀 Pickaday 實體的代碼分離並散落在實體中,在開發時實體上充斥著這些與此實體無關的代碼,造成開發上的麻煩
為了避免這個問題,我們可以使用 $once 在建立 Pickaday 實體的同時去註冊銷毀的事件:
new Vue({
  el: '#app',
  data: {
    date: null,
    // picker: null
  },
  mounted: function () {
    // this.picker =
    const picker = new Pikaday({
      field: this.$refs.dateInput,
      format: 'YYYY-MM-DD'
    });
    this.$once('hook:beforeDestroy', function() {
      picker.destroy();
    });
  },
  // beforeDestroy: function() {
  //   this.picker.destroy();
  // }
});
這樣不僅可以減少 picker 資料物件,也可以使代碼集中至真正處理的地方,以避免模糊焦點。
以下列出全部的鉤子事件;
// https://github.com/vuejs/vue/blob/dev/src/shared/constants.js#L9
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]
# 事件監聽方法
上面說明使用情境,現在來講解各個方法的使用。
# vm.$on( event, callback )
 註冊事件及其叫用的函式。
<div id="app">
  <button @click="$emit('on')">on</button>
  <button @click="$emit('on2')">on2</button>
</div>
var vm = new Vue({
  el: '#app',
  mounted: function() {
    let echoFunc = (echoStr) => () => {alert(echoStr)};
    let echoOn = echoFunc('on');
    let echoOn2 = echoFunc('on2');
    this.$on('on', echoOn);
    this.$on(['on', 'on2'], echoOn2);
  }
});
- 使用 $on註冊on事件,當觸發on事件時,會觸發echoOn方法
- $on註冊事件支援陣列形式,可以同時對多個事件註冊相同的方法,例子中使用- echoOn2同時註冊- on及- on2事件
# vm.$once( event, callback )
 註冊事件及其叫用的函式,但只觸發一次。
<div id="app">
  <button @click="$emit('once')">once</button>
</div>
var vm = new Vue({
  el: '#app',
  mounted: function() {
    let echoFunc = (echoStr) => () => {alert(echoStr)};
    let echoOnce = echoFunc('once');
    this.$once('once', echoOnce);
  }
});
使用 $once 監聽的事件只會觸發一次。
# vm.$off( [event, callback] )
 刪除監聽事件。
<div id="app">
  <button @click="$emit('once')">once</button>
  <button @click="$emit('on')">on</button>
  <button @click="$emit('on2')">on2</button>
  <button @click="$emit('off-all')">off all</button>
  <button @click="$emit('off-on')">off on</button>
  <button @click="$emit('off-echo-on2')">off echo on2</button>  
  <button @click="$emit('off-on-and-once')">off on and once</button>
</div>
var vm = new Vue({
  el: '#app',
  mounted: function() {
    let echoFunc = (echoStr) => () => {alert(echoStr)};
    
    let echoOnce = echoFunc('once');    
    
    let echoOn = echoFunc('on');
    let echoOn2 = echoFunc('on2');
    
    this.$once('once', echoOnce);
    
    this.$on('on', echoOn);
    this.$on(['on', 'on2'], echoOn2);
    
    this.$on('off-all', () => {
      this.$off(); // 註銷此實體上所有事件監聽器
    });
    
    this.$on('off-on', () => {
      this.$off('on'); // 註銷特定名稱事件監聽器
    });
    
    this.$on('off-echo-on2', () => {
      this.$off('on', echoOn2); // 註銷特定名稱的特定事件監聽器
    });
    
    this.$on('off-on-and-once', () => {
      this.$off(['on', 'once']); // 註銷多個特定名稱的事件監聽器
    });
  }
});
- 如果沒有任何參數,刪除此實體上所有事件監聽器
- 第一個參數可以指定特定名稱的事件,同時可以是陣列以選擇多個特定名稱的事件
- 第二個參數如果沒有指定,會將特定名稱的所有事件刪除
- 第二個參數如果有指定事件,則將其特定事件刪除
# 代碼
- CodePen: Listener (opens new window)
- CodePen: Dynamic Listener (opens new window)
- CodePen: Vue pikaday (opens new window)
# 總結
使用方法註冊事件監聽器使 Vue.js 的事件處理更加的靈活,並且在跟其他第三方庫或是現存專案的整合上更有彈性。