Vue實例方法之事件的實現


開始

這段時間一直在看vue的源碼,源碼非常多和雜,所以自己結合資料和理解理出了一個主線,然后根據主線去剝離其他的一些知識點,然后將各個知識點逐一學習。這里主要是分析的Vue事件處理的實現。

正文

一、了解使用方式

在分析之前先了解下幾個api的使用方式:

vm.$on(event, callback)
  • 參數

    • {string | Array<string>} event (數組只在 2.2.0+ 中支持)
    • {Function} callback
  • 用法$on事件需要兩個參數,一個是監聽的當前實例上的事件名,一個是事件觸發的回調函數,回調函數接受的是在事件出發的時候額外傳遞的參數。
  • 例子:

vm.$on('test', function (msg) {
  console.log(msg)
})
vm.$emit('test', 'hi')
// =&gt; "hi"
vm.$once(event, callback)

$once事件整體上來說和$on事件的使用方式差不多,但是event只支持字符串也就是說只支持單個事件。並且該事件再觸發一次后就移除了監聽器。

  • 例子

vm.$once('testonce', function (msg) {
  console.log(msg)
})
vm.$off([event, callback])
  • 參數

    • {string | Array<string>} event(僅在 2.2.2+ 支持數組)
    • {Function} [callback]
  • 用法:移除自定義事件監聽器

    • 如果沒有提供參數,則移除所有的事件監聽器
    • 如果只提供了事件,則移除該事件所有的監聽器;
    • 如果同時提供了事件與回調,則只移除這個回調的監聽器。
  • 例子

vm.$off()
vm.$off('test')
vm.$off('test1', function (msg) {
  console.log(msg)
})
vm.$off(['test1','test2'], function (msg) {
  console.log(msg)
})
vm.$emit(event, [..args])
  • 參數

    • {string} event 要觸發的事件名
    • [...args]可選
  • 用法

觸發當前實例上的事件。附加參數都會傳給監聽器回調。

  • 例子

vm.$emit('test', '觸發自定義事件')

二、源碼分析

事件的初始化工作

我們在使用自定義事件的api的時候,肯定有個地方是需要來存我們的事件和回調的地方。在vue中大部分的初始化工作都是在core/instance/init.jsinitMixin方法中。所以我們能夠在initMixin看到initEvents方法。


// initEvents
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的空對象。后面我們自己調用的自定義事件都存在里面。

因為vue本身在組件嵌套的時候就有子組件使用父組件的事件的時候。所以就可以通過updateComponentListeners方法把父組件事件監聽器(比如click)傳遞給子組件。(這里不做過多討論)

自定義事件的掛載方式

自定義事件的掛載是在eventsMixin方法中實現的。這里面將四個方法掛在Vue的原型上面。


Vue.prototype.$on
Vue.prototype.$once
Vue.prototype.$off
Vue.prototype.$emit
Vue.prototype.$on的實現

Vue.prototype.$on = function (event: string | Array&lt;string&gt;, fn: Function): Component {
const vm: Component = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i &lt; 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
}

第一個參數就是自定義事件,因為可能是數組,所以判斷如果是數組,那么就循環調用this.$on方法。
如果不是數組,那么就直接向最開始定義的_events對象集合里面添加自定義事件。

所以這個時候_events對象生成的格式大概就是下面:


vm._events={
    'test':[fn,fn...],
    'test1':[fn,fn...]
}
Vue.prototype.$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
}

這里定義了一個on函數。接着把fn賦值給on.fn。最后在調用的是vm.$on。這里傳入的就是事件名和前面定義的on函數。on函數在執行的時候會先移除_events中對應的事件,然后調用fn

所以分析下得到的是:


vm._events={
    'oncetest':[ 
          function on(){
              vm.$off(event,on)
              fn.apply(vm,arguments)
          } ,
          ...
     ]
}
Vue.prototype.$off的實現

Vue.prototype.$off = function (event?: string | Array&lt;string&gt;, fn?: Function): Component {
    const vm: Component = this
    // all
    // 如果沒有傳任何參數的時候,直接清楚所有掛在_events對象上的所有事件。
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    // 如果第一個參數是數組的話,那么就循環調用this.$off方法
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i &lt; l; i++) {
        this.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    // 獲取對應事件所有的回調可能是個數組
    const cbs = vm._events[event]
    // 沒有相關的事件的時候直接返回vm實例
    if (!cbs) {
      return vm
    }
    // 如果只傳入了事件名,那么清除該事件名下所有的事件。 也就是說 vm._events = {'test': null, ...}
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // 如果傳入的第二個參數為真,那么就去cbs里面遍歷,在cbs中找到和fn相等的函數,然后通過splice刪除該函數。
    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.prototype.$emit 的實現

Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event &amp;&amp; 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}".`
        )
      }
    }
    // 匹配到事件列表,該列表是一個json。
    let cbs = vm._events[event]
    if (cbs) {
      //將該json轉化成為真正的數組
      cbs = cbs.length &gt; 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      // 循環遍歷調用所有的自定義事件。
      for (let i = 0, l = cbs.length; i &lt; l; i++) {
        try {
          cbs[i].apply(vm, args)
        } catch (e) {
          handleError(e, vm, `event handler for "${event}"`)
        }
      }
    }
    return vm
}

上面主要意思是:匹配到json中相關key值的value,這個value先轉換成真正的數組,再循環遍歷數組,傳入給的參數執行數組中的每個函數

最后

vue中的自定義事件主要目的是為了組件之間的通信。因為_events對象是掛在Vue實例上的。因此每個組件是都可以訪問到vm._events的值的,也能夠向其中push值的。

整個自定義事件系統呢就是在vm實例上掛載一個_events的對象,可以理解為一個json,其中json的key值就是自定義事件的名稱,一個key值可能對應着多個自定義事件,因此json中每個key對應的value都是一個數組,每次執行事件監聽都會向數組中push相關的函數,最終通過$emit函數傳入的參數,匹配到json中相應的key,val值,從而使用給定的參數執行數組中的函數

最后的_events對象:


vm._events={
    'test1':[fn,fn,fn],
    'test2':[fn],
    'oncetest':[ 
          function on(){
              vm.$off(event,on)
              fn.apply(vm,arguments)
          },
          ... 
     ],
     ...
}

原文地址:https://segmentfault.com/a/1190000014838345


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM