Vue.js原始碼學習三 —— 事件 Event 學習

VioletJack發表於2018-03-07

早上好!繼續學習Vue原始碼~這次我們來學習 event 事件。

原始碼簡析

其實看了前兩篇的同學已經知道原始碼怎麼找了,這裡再提一下。
先找到Vue核心原始碼index方法 src/core/instance/index.js

function Vue (options) {
  if (process.env.NODE_ENV !== `production` &&
    !(this instanceof Vue)
  ) {
    warn(`Vue is a constructor and should be called with the `new` keyword`)
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

index方法中定義了一個Vue的建構函式執行 _init 方法初始化,然後執行了多個 xxxMixin 方法,這些方法是為Vue 的建構函式定義各類屬性的。比如我們今天關注的事件,Vue的幾個事件方法都是在 eventsMixin 中定義的。

export function eventsMixin (Vue: Class<Component>) {
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    ……
  }

  Vue.prototype.$once = function (event: string, fn: Function): Component {
    ……
  }

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    ……
  }

  Vue.prototype.$emit = function (event: string): Component {
    ……
  }
}

另外要注意的是,initMixin 方法中定義了Vue的初始化方法 _init,該方法中對Vue各類屬性進行了初始化。

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    initProxy(vm)
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, `beforeCreate`)
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, `created`)
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

所以,在本篇部落格中只需要關注 initEventseventsMixin 方法即可

initEvents

初始化過程很簡單,清空資料,並初始化連線父級的事件。

// src/core/instance/events.js
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)
  }
}

我深入看了下 updateComponentListeners 方法,最終走到了 src/core/vdom/helpers/update-listeners.jsupdateListeners 方法中,因為並沒有傳 oldOn 引數,所以我簡化了下程式碼,簡化程式碼如下:

// src/core/vdom/helpers/update-listeners.js
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 = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)

    if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur)
      }
      add(event.name, cur, event.once, event.capture, event.passive, event.params)
    }
  }
}

其中這個add方法如下:

// src/core/instance/events.js
// target 臨時引用vm,用完後即變為undefined
var target;

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

整理下來就是將父級的事件定義到當前vm中。

$on

監聽當前例項上的自定義事件。事件可以由vm.$emit觸發。回撥函式會接收所有傳入事件觸發函式的額外引數。

程式碼如下

  // src/core/instance/events.js
  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)
      // 通過使用標記為註冊而不是雜湊查詢的布林標記來優化鉤子 hook: 事件成本。
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

如果 event 是陣列則遍歷執行 $on 方法(2.2.0+ 中支援);
否則 向 vm._events[event] 中傳遞迴調函式 fn,這裡既然 vm._events[event] 是一個陣列,那麼我猜想一個 event 可以執行多個回撥函式咯?
如果是 event 字串中有 hook:,修改 vm._hasHookEvent 的狀態。如果 _hasHookEvent 為 true,那麼在觸發各類生命週期鉤子的時候會觸發如 hook:created 事件,這只是一種優化方式,與我們主題關係不大,具體請看程式碼~

// src/core/instance/lifecycle.js
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)
  }
}

$once

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

程式碼

  // src/core/instance/events.js
  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 事件監聽,回撥函式中使用 $off 方法取消事件監聽,並執行回撥函式。

$off

移除自定義事件監聽器。

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

程式碼如下,分析見註釋。

  // src/core/instance/events.js
  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // 如果沒有引數,關閉全部事件監聽器
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // 關閉陣列中的事件監聽器
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$off(event[i], fn)
      }
      return vm
    }
    // 具體某個事件監聽
    const cbs = vm._events[event]
    // 沒有這個監聽事件,直接返回vm
    if (!cbs) {
      return vm
    }
    // 沒有 fn,將事件監聽器變為null,返回vm
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // 有回撥函式
    if (fn) {
      // specific handler
      let cb
      let i = cbs.length
      while (i--) {
        // cbs = vm._events[event] 是一個陣列
        cb = cbs[i]
        if (cb === fn || cb.fn === fn) {
          // 移除 fn 這個事件監聽器
          cbs.splice(i, 1)
          break
        }
      }
    }
    return vm
  }

$emit

觸發當前例項上的事件。附加引數都會傳給監聽器回撥。

程式碼

  // src/core/instance/events.js
  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      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
  }

程式碼分析:首先獲取 vm._events[event] ,之前我們說過這玩意是個陣列;如果有這個事件監聽器,從第二個引數開始獲取作為觸發方法的傳參 args,遍歷事件監聽器陣列傳參執行回撥函式。

最後

就這麼多啦~其實事件還是很簡單的。明後天研究研究渲染這個難點!我們後天見!

Vue.js學習系列

鑑於前端知識碎片化嚴重,我希望能夠系統化的整理出一套關於Vue的學習系列部落格。

Vue.js學習系列專案地址

本文原始碼已收入到GitHub中,以供參考,當然能留下一個star更好啦^-^。
https://github.com/violetjack/VueStudyDemos

關於作者

VioletJack,高效學習前端工程師,喜歡研究提高效率的方法,也專注於Vue前端相關知識的學習、整理。
歡迎關注、點贊、評論留言~我將持續產出Vue相關優質內容。

新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571…
CSDN: http://blog.csdn.net/violetja…
簡書: http://www.jianshu.com/users/…
Github: https://github.com/violetjack

相關文章