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 事件上。

总结

在模板编译阶段会以属性的形式存在,在真实节点渲染阶段会根据事件属性去绑定相关的事件。对于组件的自定义事件来说,我们可以用事件进行父子组件间的通信,其实质是在子组件内部自己派发事件,监听事件。能达到通信的效果,是因为回调函数是在父组件中声明的。

posted @ 2019-11-06 16:47  奔走_在路上  阅读(871)  评论(0编辑  收藏  举报