v-on实现原理分析

一、背景

​ v-on指令在vue中用于绑定DOM/自定义事件的响应处理函数,可以用在HMTL原生标签上,也可以用于自定义的vue组件标签上。

​ 先看一下简单的使用示例:

// App.vue
<template>
  <div id="app">
    <button @click="handleButtonClick"></button>
    <HelloWorld @customEvent="handleCustomEvent" />
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld'

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  methods: {
    handleButtonClick(){
      console.log('button clicked.')
    },
    handleCustomEvent(){
      console.log('custom event received.')
    }
  },
}
</script>


// HelloWorld.vue
<template>
  <div class="hello">
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  mounted() {
    this.$emit('customEvent')
  }
}
</script>

​ 那么v-on具体是如何实现的呢,通过源码来分析一波。

二、父组件模板编译时的处理

​ 从上面示例中看到,v-on指令是在模板(即template标签中)的,只要在模板中的事物,一定会经过模板编译的处理,模板编译大致上可以分为AST和Codegen。

2.1:AST处理

​ v-on指令,作为标签上的属性,在AST时经过processAttrs方法处理

// vue-2.6.11源码 src\compiler\parser\index.js
// 仅贴出部分相关代码
function processAttrs (el){
  const list = el.attrsList
  let i, l, name, rawName, value, modifiers, syncGen, isDynamic
  for (i = 0, l = list.length; i < l; i++) {
      name = rawName = list[i].name
      value = list[i].value
      // line 844-850
      if (onRE.test(name)) { // v-on
        name = name.replace(onRE, '')
        isDynamic = dynamicArgRE.test(name)
        if (isDynamic) {
          name = name.slice(1, -1)
        }
        addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
      }
  }
}

// vue-2.6.11源码 src\compiler\parser\helper.js
// 仅贴出部分相关代码
export function addHandler (
  el: ASTElement,
  name: string,
  value: string,
  modifiers: ?ASTModifiers,
  important?: boolean,
  warn?: ?Function,
  range?: Range,
  dynamic?: boolean
) {
  modifiers = modifiers || emptyObject

  // 省略很多处理事件修饰符的代码 
  
  let events
  if (modifiers.native) {
    delete modifiers.native
    events = el.nativeEvents || (el.nativeEvents = {})
  } else {
    events = el.events || (el.events = {})
  }
    
  const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
  if (modifiers !== emptyObject) {
    newHandler.modifiers = modifiers
  }

  const handlers = events[name]
  /* istanbul ignore if */
  if (Array.isArray(handlers)) {
    important ? handlers.unshift(newHandler) : handlers.push(newHandler)
  } else if (handlers) {
    events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
  } else {
    events[name] = newHandler
  }

  el.plain = false
}

​ AST处理后,会在对应的AST节点上生成events属性,events属性是一个对象,key值是v-on绑定的事件名称,值是事件的响应函数。

2.2:Codegen处理

​ AST处理后生成的节点树,会经过Codegen处理生成渲染函数

// vue-2.6.11源码 src\compiler\codegen\index.js
// 仅贴出部分相关代码
export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true)},`
  }
  
  return data
}

// vue-2.6.11源码 src\compiler\codegen\event.js
export function genHandlers (
  events: ASTElementHandlers,
  isNative: boolean
): string {
  const prefix = isNative ? 'nativeOn:' : 'on:'
  let staticHandlers = ``
  let dynamicHandlers = ``
  for (const name in events) {
    const handlerCode = genHandler(events[name])
    if (events[name] && events[name].dynamic) {
      dynamicHandlers += `${name},${handlerCode},`
    } else {
      staticHandlers += `"${name}":${handlerCode},`
    }
  }
  staticHandlers = `{${staticHandlers.slice(0, -1)}}`
  if (dynamicHandlers) {
    return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])`
  } else {
    return prefix + staticHandlers
  }
}

​ 总结:codegen时递归的对每一个AST节点进行处理。针对events属性,最终的data属性中有一个on属性(如果有native事件,还会有nativeOn属性),on属性的值也是一个对象,其中的key值是事件名称,value值是事件响应函数。

2.3:最终生成的render函数

​ 我们看一下App.vue实际上生成的render函数:

// App.vue 编译后的render函数
var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    { attrs: { id: "app" } },
    [
      _c("button", { on: { click: _vm.handleButtonClick } }),
      _vm._v(" "),
      _c("HelloWorld", { on: { customEvent: _vm.handleCustomEvent } })
    ],
    1
  )
}

​ 从以上渲染函数可以看出,button和HelloWorld节点生成了data数据,其中有on属性,和源码中分析的情况一致。

三、自定义组件中的事件处理

​ 在实例化vue组件时,会进行初始化处理,在初始化时就会对events属性进行处理

// vue-2.6.11源码 src\core\instance\events.js
export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // 渲染函数中的data参数(createELement的第二个参数),经过处理,会成为属性挂在$options上
  //_parentListeners属性对应的就是on属性
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

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

function add (event, fn) {
  // 调用原型上的$on方法注册事件和响应,所以直接使用vm.$on也可以进行事件绑定注册
  target.$on(event, fn)
}

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++) {
        vm.$on(event[i], fn)
      }
    } else {
      // 重点:最终事件和响应存储在vm._events属性中
      (vm._events[event] || (vm._events[event] = [])).push(fn)
    }
    return vm
  }

// vue-2.6.11源码 src\core\vdom\helpers\update-listeners.js
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) {
    def = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)
    /* istanbul ignore if */
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    } 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方法
      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)
    }
  }
}

​ 总结起来就是,事件和对应的响应最终存储在vm._events属性中。

​ 我们知道事件在子组件中通过emit触发,接下来看一下emit的代码:

// vue-2.6.11源码 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)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }

​ 可以看到$emit的逻辑很简单,就是从vm._events属性中取对应事件的响应函数,然后执行。

四、原生标签上的事件处理

​ 上一节我们查看了vue组件中对events的处理过程,但是这些仅限于vue组件;

​ 对于原生标签使用了更为简单的处理,即使用原生DOM自带的addEventListener API:

//vue-2.6.11源码 src\platforms\web\runtime\modules\events.js
function add (
  name: string,
  handler: Function,
  capture: boolean,
  passive: boolean
) {
  if (useMicrotaskFix) {
    const attachedTimestamp = currentFlushTimestamp
    const original = handler
    handler = original._wrapper = function (e) {
      if (
        e.target === e.currentTarget ||
        e.timeStamp >= attachedTimestamp ||
        e.timeStamp <= 0 ||
        e.target.ownerDocument !== document
      ) {
        return original.apply(this, arguments)
      }
    }
  }
  // 使用addEventListener API进行事件绑定
  target.addEventListener(
    name,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

​ 总结:原生标签采用addEventListener进行事件注册。