Vue源碼探究-事件系統

Vue源碼探究-事件系統

本篇代碼位於vue/src/core/instance/events.js

緊跟着生命週期之後的就是繼續初始化事件相關的屬性和方法。整個事件系統的代碼相對其他模塊來說非常簡短,分幾個部分來詳細看看它的具體實現。

頭部引用

import {
  tip,
  toArray,
  hyphenate,
  handleError,
  formatComponentName
} from '../util/index'
import { updateListeners } from '../vdom/helpers/index'

頭部先是引用了的一些工具方法,沒有什麼難點,具體可以查看相應文件。唯一值得注意的是引用自虛擬節點模塊的一個叫 updateListeners 方法。顧名思義,是用來更新監聽器的,至於爲什麼要有這樣的一個方法,主要是因爲如果該實例的父組件已經存在一些事件監聽器,爲了正確捕獲到事件並向上冒泡,父級事件是需要繼承下來的,這個原因在下面的初始化代碼中有佐證;另外,如果在實例初始化的時候綁定了同名的事件處理器,也需要爲同名事件添加新的處理器,以實現同一事件的多個監聽器的綁定。

事件初始化

// 定義並導出initEvents函數,接受Component類型的vm參數
export function initEvents (vm: Component) {
  // 創建例的_events屬性,初始化爲空對象
  vm._events = Object.create(null)
  // 創建實例的_hasHookEvent屬性,初始化爲false
  vm._hasHookEvent = false
  // 初始化父級附屬事件
  // init parent attached events
  const listeners = vm.$options._parentListeners
  // 如果父級事件存在,則更新實例事件監聽器
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

// 設置target值,目標是引用實例
let target: any

// 添加事件函數,接受事件名稱、事件處理器、是否一次性執行三個參數
function add (event, fn, once) {
  if (once) {
    target.$once(event, fn)
  } else {
    target.$on(event, fn)
  }
}

// 移除事件函數,接受事件名稱和時間處理器兩個參數
function remove (event, fn) {
  target.$off(event, fn)
}

// 定義並導出函數updateComponentListeners,接受實例對象,新舊監聽器參數
export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  // 設置target爲vm
  target = vm
  // 執行更新監聽器函數,傳入新舊事件監聽對象、添加事件與移除事件函數、實例對象
  updateListeners(listeners, oldListeners || {}, add, remove, vm)
  // 置空引用
  target = undefined
}

如上述代碼所示,事件監聽系統的初始化首先是創建了私有的事件對象和是否有事件鉤子的標誌兩個屬性,然後根據父級是否有事件處理器來決定是否更新當前實例的事件監聽器,具體如何實現監聽器的更新,貼上這段位於虛擬節點模塊的輔助函數中的代碼片段來仔細看看。

更新事件監聽器

// 定義並導出updateListeners哈數
// 接受新舊事件監聽器對象,事件添加和移除函數以及實例對象參數。
export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  vm: Component
) {
  // 定義一些輔助變量
  let name, def, cur, old, event
  // 遍歷新的監聽器對象
  for (name in on) {
    // 爲def和cur賦值爲新的事件對象
    def = cur = on[name]
    // 爲old賦值爲舊的事件對象
    old = oldOn[name]
    // 標準化事件對象並賦值給event。
    // normalizeEvent函數主要用於將傳入的帶有特殊前綴的事件修飾符分解爲具有特定值的事件對象
    event = normalizeEvent(name)
    // 下面代碼是weex框架專用,處理cur變量和格式化好的事件對象的參數屬性
    /* istanbul ignore if */
    if (__WEEX__ && isPlainObject(def)) {
      cur = def.handler
      event.params = def.params
    }
    // 如果新事件不存在,在非生產環境中提供報錯信息,否則不執行任何操作
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    // 當舊事件不存在時
    } else if (isUndef(old)) {
      // 如果新事件對象cur的fns屬性不存在
      if (isUndef(cur.fns)) {
        // 創建函數調用器並重新複製給cur和on[name]
        cur = on[name] = createFnInvoker(cur)
      }
      // 添加新的事件處理器
      add(event.name, cur, event.once, event.capture, event.passive, event.params)
    // 如果新舊事件不完全相等
    } else if (cur !== old) {
      // 用新事件處理函數覆蓋舊事件對象的fns屬性
      old.fns = cur
      // 將事件對象重新複製給on
      on[name] = old
    }
  }
  // 遍歷舊事件監聽器
  for (name in oldOn) {
    // 如果新事件對象不存在
    if (isUndef(on[name])) {
      // 標準化事件對象
      event = normalizeEvent(name)
      // 移除事件處理器
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

這段代碼中用到了 normalizeEventcreateFnInvoker 兩個主要的函數來完成更新監聽器的實現,代碼與 updateListeners 函數位於同一文件中。

  • normalizeEvent:主要是用於返回一個定製化的事件對象,這個函數接受4個必選參數和2兩個可選參數,分別是事件名稱name屬性、是否一次性執行的once屬性、是否捕獲事件的capture屬性、是否使用被動模式passive屬性、事件處理器handler方法、事件處理器參數params數組。屬性的含義都比較好理解,特別注意一下 oncecapturepassive 屬性,這三個屬性是用來修飾事件的,分別對應了 ~!& 修飾符,貼上一個官方文檔中的使用示例,引用自事件 & 按鍵修飾符。啓動被動模式的用途是使事件處理器無法阻止默認事件,比如 <a> 標籤自帶的鏈接跳轉事件,如果設置passive爲true,則事件處理器即便是設置了阻止默認事件也是沒辦法阻止跳轉的。
on: {
  '!click': this.doThisInCapturingMode,
  '~keyup': this.doThisOnce,
  '~!mouseover': this.doThisOnceInCapturingMode
}
  • createFnInvoker: 接受一個fns參數,可以傳入一個事件處理器函數,也可以傳入一個包含多個處理器的數組。在該函數內部定義了一個 invoker 函數並且最終返回它,函數有一個fns屬性是用來存放所傳入的處理器的,調用這個函數後,會按fns的類型來分別執行處理器數組的調用或單個處理器的調用。這個實現即是真正執行事件處理器調用的過程。

事件相關的原型方法

在事件的初始化過程裏有用到幾個以 & 開頭的類原型方法,它們是在mixin函數裏掛載到核心類上的。初始化的時候定義的方法都是在這些方法的基礎上再進行了一次封裝,其綁定事件、觸發事件和移除事件的具體實現都在這些方法中,當然不會放過對這些細節的探索。

// 導出eventsMixin函數,接收形參Vue,
// 使用Flow進行靜態類型檢查指定爲Component類
export function eventsMixin (Vue: Class<Component>) {
  // 定義hook正則檢驗
  const hookRE = /^hook:/
  // 給Vue原型對象掛載$on方法
  // 參數event可爲字符串或數組類型,fn是事件監聽函數
  // 方法返回實例對象本身
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    // 定義實例變量
    const vm: Component = this
    // 如果傳入的event參數是數組,遍歷event數組,爲所有事件註冊fn監聽函數
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$on(event[i], fn)
      }
    } else {
      // event參數爲字符串時,檢查event事件監聽函數數組是否存在
      // 已存在事件監聽數組則直接添加新監聽函數
      // 否則建立空的event事件監聽函數數組,再添加新監聽函數
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // 此處做了性能優化,使用正則檢驗hook:是否存在的布爾值
      // 而不是hash值查找設置實例對象的_hasHookEvent值
      // 此次優化是很久之前版本的修改,暫時不太清楚以前hash值查找是什麼邏輯,留待以後查證
      // 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
  }
  // 爲Vue原型對象掛載$once方法
  // 參數event只接受字符串,fn是監聽函數
  Vue.prototype.$once = function (event: string, fn: Function): Component {
    // 定義實例變量
    const vm: Component = this
    // 創建on函數
    function on () {
      // 函數執行後先清除event事件綁定的on監聽函數,即函數本身
      // 這樣以後就不會再繼續監聽event事件
      vm.$off(event, on)
      // 在實例上運行fn監聽函數
      fn.apply(vm, arguments)
    }
    // 爲on函數設置fn屬性,保證在on函數內能夠正確找到fn函數
    on.fn = fn
    // 爲event事件註冊on函數
    vm.$on(event, on)
    // 返回實例本身
    return vm
  }
  // 爲Vue原型對象掛載$off方法
  // event參數可爲字符串或數組類型
  // fn是監聽函數,爲可選參數
  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    // 定義實例變量
    const vm: Component = this
    // 如果沒有傳入參數,則清除實例對象的所有事件
    // 將實例對象的_events私有屬性設置爲null,並返回實例
   // all
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // 如果event參數傳入數組,清除所有event事件的fn監聽函數返回實例
    // 這裏是$off方法遞歸執行,最終會以單一事件爲基礎來實現監聽的清除
    // array of events
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$off(event[i], fn)
      }
      return vm
    }
    // 如果指定單一事件,將事件的監聽函數數組賦值給cbs變量
    // specific event
    const cbs = vm._events[event]
    // 如果沒有註冊此事件監聽則返回實例
    if (!cbs) {
      return vm
    }
    // 如果沒有指定監聽函數,則清除所有該事件的監聽函數,返回實例
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // 如果指定監聽函數,則遍歷事件監聽函數數組,移除指定監聽函數返回實例
    if (fn) {
      // specific handler
      let cb
      let i = cbs.length
      while (i--) {
        cb = cbs[i]
        if (cb === fn || cb.fn === fn) {
          cbs.splice(i, 1)
          break
        }
      }
    }
    return vm
  }
  // 爲Vue原型對象掛載$emit方法,只接受單一event
  Vue.prototype.$emit = function (event: string): Component {
    // 定義實例變量
    const vm: Component = this
    // 在非生產環境下,傳入的事件字符串如果是駝峯值且有相應的小寫監聽事件
    // 則提示事件已註冊,且無法使用駝峯式註冊事件
    if (process.env.NODE_ENV !== 'production') {
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    // 將事件監聽函數數組賦值 給cbs
    let cbs = vm._events[event]
    // 如果監聽函數數組存在
    if (cbs) {
      // 重置cbs變量,爲何要使用toArray方法轉換一次數組不太明白?
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      // 將event之後傳入的所有參數定義爲args數組
      const args = toArray(arguments, 1)
      // 遍歷所有監聽函數,爲實例執行每一個監聽函數,並傳入args參數數組
      for (let i = 0, l = cbs.length; i < l; i++) {
        try {
          cbs[i].apply(vm, args)
        } catch (e) {
          handleError(e, vm, `event handler for "${event}"`)
        }
      }
    }
    return vm
  }
}

eventsMixin的內容非常直觀,分別爲實例原型對象掛載了$on$once$off$emit四個方法。這是實例事件監聽函數的註冊、一次性註冊、移除和觸發的內部實現。在使用的過程中會對這些實現有一個更清晰的理解。


終於對Vue的事件系統的實現有了一個大致瞭解,沒有什麼特別高深的處理,但完整的事件系統的實現有很多細緻的功能這裏其實並沒有特別詳細地探討,比如事件修飾符,可以參考官方文檔裏的解說會有一個更清晰的瞭解。事件系統的重要作用首先是爲實例制定了一套處理事件的方案和標準,其次是在實例數據更新的過程中保持對事件監聽器的更新,這兩個部分的處理是最需要細緻去琢磨的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章