VUE源碼——事件機制


VUE是怎么樣處理事件的

在日常的開發中,我們把 @click 用的飛起,組件自定義事件實現父子組件之間的通信,那我們有想過其中的實現原理是什么呢?接下來我們將探索原生事件和自定義事件的奧秘。帶着疑問開始擼源碼。

首先來點兒測試代碼,在測試代碼中,我們包含了原生的事件,和自定義事件

<body>
  <div id="app">
    <h1>事件處理</h1>
    <!-- 普通事件 -->
    <p @click='onclick'>普通事件</p>
    <!-- 自定義事件 -->
    <comp @myclick="onMyClick"></comp>
  </div>
</body>
<script>
  Vue.component('comp', {
    template: `<div @click="onClick">this is comp</div>`,
    methods: {
      onClick() {
        this.$emit('myclick')
      }
    }
  })
  const app = new Vue({
    el: '#app',
    methods: {
      onclick() {
        console.log('普通事件');
      },
      onMyClick() {
        console.log('自定義事件');
      }
    },
  })
  console.log(app.$options.render);
</script>

在Vue 掛載之前做了許多編譯的工作,把 template 模板編譯成 render函數,這個過程就不做過多的講解。我們主要來看生產render函數后是怎么實現事件的綁定的。

我們來觀察打印出的app.$options.render 的結果

(function anonymous() {
    with(this) {
      return _c('div', {
        attrs: {
          "id": "app"
        }
      }, [_c('h1', [_v("事件處理")]), _v(" "), _c('div', {
        on: {
          "click": onclick
        }
      }, [_v("普通事件")]), _v(" "), _c('comp', {
        on: {
          "myclick": onMyClick
        }
      })], 1)
    }
  })

根據打印的結果來看,普通事件和自定義事件生成的結果其實差不多,都將事件的處理放在了on上面。

普通事件

據我所知,在Vue 組件初始化的時候,原生事件的監聽會在 platforms\web\runtime\modules\events.js里面,會執行 updateDOMListeners方法。

想要知道驗證一下,是否執行到了該函數,我們可以在函數里面打斷點驗證一下。 

 

 

可以看到,我們會成功的進入,那想要知道調用流程,我們可以在堆棧信息里面看看。

因為在有了 Vnode 過后,會遍歷子節點遞歸的調用 createElm 為每個子節點創建真實的 DOM,在創建真實的 DOM 時會組成相關的鈎子invokeCreateHooks。其中就包括注冊事件的處理 updateDOMListeners 

 

 進入到 invokeCreateHooks 函數

function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
    cbs.create[i$1](emptyNode, vnode);
  }
  i = vnode.data.hook; // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) { i.create(emptyNode, vnode); }
    if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
  }
}

我們可以看看會執行哪些鈎子函數。

 

 我們可以看到,在 invokeCreateHooks 函數里面,是把所有的鈎子函數執行一遍,其中就有 updateDOMListeners 

function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  const on = vnode.data.on || {}
  const oldOn = oldVnode.data.on || {}
  target = vnode.elm
  // 兼容性處理
  normalizeEvents(on)
  updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
  target = undefined
}

其中 normalizeEvents 是對 v-model 的兼容性處理,在 IE 下沒有 input 只支持 change 事件,把 input 事件替換成 change 事件。

if (isDef(on[RANGE_TOKEN])) {
    // IE input[type=range] only supports `change` event
    const event = isIE ? 'change' : 'input'
    on[event] = [].concat(on[RANGE_TOKEN], on[event] || [])
    delete on[RANGE_TOKEN]
  }

updateListeners 的邏輯也不復雜,它會遍歷on事件對新節點事件綁定注冊事件,對舊節點移除事件監聽。

 

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component
) {
  let name, def, cur, old, event
  for (name in on) {
    ...
    //  執行真正注冊事件的執行函數
      add(event.name, cur, event.capture, event.passive, event.params)
    } else if (cur !== old) {
      old.fns = cur
      on[name] = old
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

add 函數,是在真正的 DOM 上綁定事件,它的實現也是利用了原生 DOM 的 addEventListener

function add (
  name: string,
  handler: Function,
  capture: boolean,
  passive: boolean
) {
  ...
  
  target.addEventListener(
    name,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

 

 

一目了然,將事件添加到原生的click事件上,並實現了監聽。 以上就是普通事件綁定的流程。

自定義事件

我們知道,父子組件可以使用事件進行通信,子組件通過vm.$emit 向父組件派發事件,父組件通過v-on:(event)接受信息並處理回調。

從最開始的例子中可以看出,普通節點使用的原生DOM事件,在組件上可以使用自定義事件,另外組件上還可以使用原生事件,用 .native 修飾符區分。 接下來我們看看自定義事件是怎么處理的。

Vnode 生成真實節點的過程中,這個過程遇到子Vnode會實例化子組件實例。實例化子類構造器的過程,會有初始化選項配置的過程,會進入到Vue.prototype.init,我們直接看對自定義事件的處理。 在 src\core\instance\init.js

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    ...
    // merge options
    // 針對子組件的事件處理
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    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')

    ...
  }

進入到事件處理函數 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)
  }
}

我們把斷點打到函數里面看一看 

 

 第一次進去的時候,我們看到當前創建的是根組件,根組件的 _uid:0, 我們放過再進一次,現在看到的就是我們自定義的組件在創建。這時候的 listeners 會存在。

 

 接下來會進去 updateComponentListeners,自定義事件的處理。

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

簡單的看這段代碼,把當前組件實例賦值給目標對象 target, 然后進行事件監聽。

同樣的,會有 add 函數執行,那這里的 add 和原生事件的又所不同,我們可以猜想一下,這里的 add 是怎么處理的。

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component
) {
  let name, def, cur, old, event
  for (name in on) {
    ...
    } else if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur, vm)
      }
      if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture)
      }
      add(event.name, cur, event.capture, event.passive, event.params)
    } else if (cur !== old) {
      old.fns = cur
      on[name] = old
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

可能也猜想到了,是通過 $on 進行事件監聽

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

我們可以看到,自定義事件,雖然是在事件監聽聲明在父組件上來,但是監聽還是在子組件上監聽的,所義誰派發,誰監聽。

那會存在疑問,自己派發,自己監聽,那是怎么和父組件經通信的呢? 這里需要注意下,回調函數是在父組件聲明的。

我們會想,子組件是怎么拿到父組件的自定義事件的呢 ,其實在updateComponentListeners  vm.$options._parentListeners,可以拿到父組件的自定義事件。那么 _parentListeners 又是怎么來的呢?

其實在 _init 方法里,執行 initEvents 之前,會對組件進行處理。initInternalComponent(vm, options)

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

在父組件里面的組件的 vnodeComponentOptions里面的 listeners就是自定義組件里面定義的事件,myClick, 這樣在子組件內部就可以拿到,然后在綁定在 on 事件上。

總結

在模板編譯階段會以屬性的形式存在,在真實節點渲染階段會根據事件屬性去綁定相關的事件。對於組件的自定義事件來說,我們可以用事件進行父子組件間的通信,其實質是在子組件內部自己派發事件,監聽事件。能達到通信的效果,是因為回調函數是在父組件中聲明的。


免責聲明!

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



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