vue源碼解析事件派發($on、$emit、$once、$off)

本文將解析vue實例上與事件派發相關的4個方法,分別爲:vue

  • $on:監聽事件
  • $off:移除監聽事件
  • $emit:觸發事件
  • $once:監聽事件,只監聽1次

首先整體介紹下這四個方法實現的基本原理:數組

一、vue實例上會建立一個對象來保存全部要監聽的事件:vm._events = {}app

二、每當咱們要監聽一個事件,就往vm._events裏添加一個鍵值對,事件的名稱做爲鍵,一個空數組做爲值。例如咱們要監聽的事件名稱爲event1,則vm._events = {event1: []}函數

三、監聽事件的回調函數都會添加到對應的數組中,例如咱們調用ui

vm.$on('event1', cb1);
vm.$on('event1', cb2);
vm.$on('event1', cb3);
複製代碼

則此時vm._events={event1: [cb1, cb2, cb3]}this

四、當調用移除監聽事件的方法時,所作的操做爲移除其對應數組裏的回調函數,例如當咱們調用spa

vm.$off('event1', cb1);
複製代碼

這時cb1就被移除了,vm._events={event1: [cb2, cb3]}prototype

五、當咱們執行$emit觸發對應事件時,所作的操做就是把該事件對應數組裏的回調函數都拿出來執行一遍,例如當咱們調用code

vm.$emit('event')
複製代碼

這時候會取出event1對應數組裏的cb2cb3執行對象

六、$once表示該事件只會觸發執行一次,後面在觸發就沒用了,例如當咱們調用:

// 用$on方法監聽event2,回調函數爲cb4
vm.$on('event2', cb4);

// 用$once方法監聽event2,回調函數爲cb5
vm.$once('event2', cb5);

// 觸發event2事件,會執行cb4和cb5
vm.$emit('event2');

// 再次觸發event2事件,這裏只會執行cb5,不會執行cb4,cb4只會執行一次
vm.$emit('event2');
複製代碼

以上即爲事件派發方法的基本原理,固然這些方法還有一些稍微複雜一點的使用方式,好比

  • $on(['event1', 'event2'], cb) // 監聽多個事件
  • $off(['event1', 'event2'], cb) // 移除多個事件
  • $off() // 移除事件不傳參數
  • $off('event1') // 移除事件傳一個參數
  • $off('event1', cb) // 移除事件傳兩個參數
  • $emit('event1', param1, param2) // 觸發事件傳參數

看完下面的源碼解析就知道這些狀況都是怎麼處理的了

下面來看下vue源碼裏這四個方法的具體實現:

$on:

Vue.prototype.$on = function (event, fn) {
    const vm = this
    
    // 咱們傳入的要監聽的事件可能爲數組,這時候對數組裏的每一個事件再遞歸調用$on方法
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
    
      // 以前已經有監聽event事件,則將這次監聽的回調函數添加到其數組中,不然建立一個新數組並添加fn
      (vm._events[event] || (vm._events[event] = [])).push(fn)
    }
    return vm
  }
複製代碼

$off:

Vue.prototype.$off = function (event, fn) {
    const vm = this
    // all
    if (!arguments.length) {
    // 若是沒有傳參數,則清空全部事件的監聽函數
      vm._events = Object.create(null)
      return vm
    }
    
    // 若是傳的event是數組,則對該數組裏的每一個事件再遞歸調用$off方法
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    
    // 獲取當前event裏全部的回調函數
    const cbs = vm._events[event]
    
    // 若是不存在回調函數,則直接返回,由於沒有能夠移除監聽的內容
    if (!cbs) {
      return vm
    }
    
    // 若是沒有指定要移除的回調函數,則移除該事件下全部的回調函數
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    
    // 指定了要移除的回調函數
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      // 在事件對應的回調函數數組裏面找出要移除的回調函數,並從數組裏移除
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }
複製代碼

$emit:

Vue.prototype.$emit = function (event) {
    const vm = this
    
    // 拿出觸發事件對應的回調函數列表
    let cbs = vm._events[event]
    
    if (cbs) {
      
      // $emit方法能夠傳參,這些參數會在調用回調函數的時候傳進去
      const args = toArray(arguments, 1)
      
      // 遍歷回調函數列表,調用每一個回調函數
      for (let i = 0, l = cbs.length; i < l; i++) {
        cbs[i].apply(vm, args)
      }
    }
    return vm
  }
}
複製代碼

$once:

Vue.prototype.$once = function (event, fn) {
    const vm = this
    
    // 封裝一個高階函數on,在on裏面調用fn
    function on () {
      // 每當執行了一次on,移除event裏的on事件,後面再觸發event事件就不會再執行on事件了,也就不會執行on裏面的fn事件
      vm.$off(event, on)
      
      // 執行on的時候,執行fn函數
      fn.apply(vm, arguments)
    }
    
    // 這個賦值是在$off方法裏會用到的
    // 好比咱們調用了vm.$off(fn)來移除fn回調函數,然而咱們在調用$once的時候,實際執行的是vm.$on(event, on)
    // 因此在event的回調函數數組裏添加的是on函數,這個時候要移除fn,咱們沒法在回調函數數組裏面找到fn函數移除,只能找到on函數
    // 咱們能夠經過on.fn === fn來判斷這種狀況,並在回調函數數組裏移除on函數
    on.fn = fn
    
    // $once最終調用的是$on,而且回調函數是on
    vm.$on(event, on)
    return vm
  }
複製代碼
相關文章
相關標籤/搜索