人人都能懂的Vue源碼系列—09—initEvents

上篇文章中,咱們主要講了initLiftcycle方法,它的做用是初始化vm實例中和生命週期相關的屬性。今天爲你們介紹另外一個方法——initEvents。
從這個方法的名稱來看,咱們知道它是和事件相關的方法,具體怎麼相關,咱們先來看源碼。html

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

先看第一行vue

vm._events = Object.create(null)

該行代碼建立了一個原型爲null的空對象,並把它賦值給vm實例的_events屬性。關於Object.create,不明白的同窗能夠點這裏查看。關於vm._events,不少博文中提到該屬性就是籠統的說是用來存放事件的對象,那麼到底存放什麼事件呢?vm的全部事件都存放在裏面?顯然是不對的,那具體是什麼,咱們來看下面的例子。
html部分:數組

<div id="app">
    <child
      @hook:created="hookFromParent"
      @hover="hoverFromParent"
      :msg-from-father="msg" 
      :hit-from-father="hit"
    >
    </child>
  </div>

js部分緩存

const childComponent = Vue.component('child', {
      template: '<div><p @click="showMsgFromSon">{{msgFromFather}} and {{hitFromFather}}</p></div>',
      data: function () {
        return {
          childMsg: 'Hello, I am Child'
        }
      },
      props: ['msgFromFather', 'hitFromFather'],
      methods: {
        showMsgFromSon () {
          console.log('Hello Vue from son')
        }
      },
      mounted () {
        console.log('child mounted')
      }
    })
    const app = new Vue({
      el: '#app',
      data: function () {
        return {
          msg: 'Hello Chris, I am your father',
          hit: 'I will hit you if you do not study',
        }
      },
      components: {
        childComponent
      },
      methods: {
        hoverFromParent () {
          console.log('attch event')
        },
        hookFromParent () {
          console.log('attch hook')
        }
      }
    })

下圖表示的是上述demo中vm._events屬性的值app

_events的值

上面例子中,child組件上除了父組件綁定的方法以外,其組件內部還有showMsgFromSon和mounted鉤子方法,可是這兩個方法都沒有出如今_events屬性中。綜上可知,vm._events表示的是父組件綁定在當前組件上的事件。
接下來看代碼dom

vm._hasHookEvent = false

這行代碼把咱們vm實例上的_hasHookEvent屬性設置爲false。該屬性表示父組件是否經過"@hook:"把鉤子函數綁定在當前組件上。該用法能夠在上個demo中找到,經過下列方式完成綁定。函數

@hook:鉤子名稱="綁定的函數"

繼續回到源碼中性能

// init parent attached events
const listeners = vm.$options._parentListeners

從英文註釋中,咱們知道這行代碼的做用是初始化父組件添加的事件。那具體是什麼意思呢?經過追蹤vm.$options._parentListeners的賦值過程(這個過程有點複雜,在以後講雙向綁定和虛擬dom的時候會說到),咱們知道vm.$options._parentListeners其實和上面的_events同樣,都是用來表示父組件綁定在當前組件上的事件。(固然仍是略有點不一樣,這個以後會講解)若是存在這些綁定的事件,那麼就執行下面代碼優化

if (listeners) {
   updateComponentListeners(vm, listeners)
 }

若是事件存在,則調用updateComponentListeners更新這些方法。this

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, vm)
  target = undefined
}

來看updateComponentListeners方法的源碼

target = vm

這行代碼的主要做用是保留對vm實例的引用,在執行updateListeners方法時能訪問到實例對象,並執行add和remove方法。

updateListeners(listeners, oldListeners || {}, add, remove, vm)

在研究updateListeners源碼以前,咱們先來了解一下傳入的這幾個參數。listeners咱們前面說過,是父組件綁定在當前組件上的事件對象,oldListeners表示當前組件上舊的事件對象,vm是vue實例對象。這三個沒什麼好說的,咱們具體來說講另外兩個參數add和remove。

add方法

add方法源碼以下:

function add (event, fn, once) {
  if (once) {
    target.$once(event, fn)
  } else {
    target.$on(event, fn)
  }
}

若是第三個參數once爲true,則執行vue.$once方法,不然執行vue.$on方法。咱們先來看vue.$on

vue.$on方法

爲何要先講$on方法,由於$once方法中也須要用到$on,在看$on源碼以前,咱們先來看看官方文檔裏對它的定義。

監聽當前實例上的自定義事件。事件能夠由vm.$emit觸發。回調函數會接收全部傳入事件觸發函數的額外參數。

知道了vue.$on的定義以後,咱們再來看源碼。

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$on(event[i], fn)
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

else以前的代碼都很簡單,先緩存this,若是傳入的事件是事件數組的話,則分別對數組內的每一項調用$on綁定事件。接下來重點看看else塊內的代碼。

(vm._events[event] || (vm._events[event] = [])).push(fn)

咱們知道_events是表示直接綁定在組件上的事件,若是是經過$on新添加的事件(也至關於直接綁定在組件上的事件),咱們也要把事件和回調方法傳入到_events對象中。
回到源碼中

// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash look
if (hookRE.test(event)) {
  vm._hasHookEvent = true
}

關於這句代碼的解釋,網上不少文章說的都是相似下面的話

這個bool標誌位來代表是否存在鉤子,而不須要經過哈希表的方法來查找是否有鉤子,這樣作能夠減小沒必要要的開銷,優化性能。

這句話除了是翻譯原文註釋以外,還存在明顯的錯誤,這個tag不是代表是否存在鉤子,而是表示是否使用下面的方式綁定鉤子。
若是是下列形式綁定的鉤子,則_hasHookEvent屬性爲true。

<child
  @hook:created="hookFromParent"
>

而像下面這種形式,它也存在鉤子函數,可是它的_hasHookEvent就是false。

const childComponent = Vue.component('child', {
      ...
      created () {
        console.log('child created')
      }
    })

因此_hasHookEvent不是表示是否存在鉤子,它表示的是父組件有沒有直接綁定鉤子函數在當前組件上。說這麼多,只是但願你們儘量的少被誤導。那麼,那句註釋究竟是什麼意思呢?咱們能夠從callHook的源碼中來尋找答案。

export function callHook (vm: Component, hook: string) {
  const handlers = vm.$options[hook]
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
}

當前實例的鉤子函數若是是經過父組件的:hook方式來指定的,那麼它在執行鉤子函數的回調方法時就是直接觸發vm.$emit來執行。(這種方式相似於dom中的addEventListener監聽事件和dispatchEvent觸發事件)
若是不是上面這種方法指定的鉤子函數,就須要執行callhook源碼上半部分的代碼邏輯。找到vm實例上的鉤子函數,而後執行綁定在它上面的回調。至於執行效率的問題,沒有去研究過,可是原文註釋裏都說了是優化鉤子,那麼證實第一種方法執行效率應該是優於第二種方法。
咱們回到$on的源碼中,最後是返回vm實例對象。

return vm

如今咱們知道了vm.$on方法主要就是把傳入的方法給push到_events屬性裏,方便以後被$emit調用。

vm.$once

講過了vm.$on的主要做用以後,咱們接着來分析vm.$once的源碼,先看文檔中關於vm.$once的定義。

監聽一個自定義事件,可是隻觸發一次,在第一次觸發以後移除監聽器。

瞭解了vm.$once的定義以後,咱們再來看源碼

Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on () {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

結合上面的定義和以前講的vm.$on方法,咱們應該比較容易理解解$once了,它和$on方法的核心區別主要在on方法

function on () {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }

on方法包裝了event的回調事件,這是on和once最本質的區別,當觸發once綁定的回調時候,執行on方法,先調用$off方法(這個方法是移除監聽的方法,咱們待會兒就會講)移除監聽,而後再執行回調函數。這樣就實現了只觸發一次的功能,講到這裏,add方法中全部的內容就已經講完了。因爲文章篇幅的緣由,其餘關於initEvents的內容咱們下篇文章繼續講,主要有$off,$emit和updateListeners相關的實現。敬請期待。

相關文章
相關標籤/搜索