Skip to content
On this page

event 事件

event 是组件或者元素由于用户交互所触发的行为

编译时(parse)

在将模板字符串编译成ast树的时候,闭合一个标签的时候,会对标签上的属性进行处理,也就是processAttrs函数

js
// compiler/parser/index.js
function processAttrs(el) {
  ...
  if (dirRE.test(name)) {
    // name: 指令原始名称 @click.native.prevent / @child-event
    el.hasBindings = true
    // 获取修饰符集合
    modifiers = parseModifiers(name.replace(dirRE, ''))
    name = name.replace(modifierRE, '') // 纯净版指令名 @click @child-event
    
    ...
    name = name.replace(onRE, '')
    // 向AST节点内部添加事件处理
    addHandler(el, name, value, modifiers, false)
  }
}

function parseModifiers(name) {
  const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g
  const match = name.match(modifierRE)
  if (match) {
    const ret = {}
    match.forEach(m => { ret[m.slice(1)] = true })
    return ret
  }
  // ret = { native: true, prevent: true }
}

上面是通过解析ast节点内的属性对来操作,得到事件名和事件修饰符

js
// compiler/helpers.js
function addHandler(el, name, value, modifiers, important) {
 // import = false
 if (modifiers.right) {
   name = 'contextmenu' // 鼠标右键
   delete modifiers.right
 } else if (modifiers.middle) {
   name = 'mouseup' // 中键
 }
 ...
 let events
 if (modifiers.native) {
   delete modifiers.native
   events = el.nativeEvents || (el.nativeEvents = {})
 } else {
   events = el.events || (el.events = {})
 }
 // 创立ast节点的events或者nativeEvents属性
 const newHandler = rangeSetItem({value: value.trim()})
 // 其实可以看作就是参数内部的那个对象
 newHandler.modifiers = modifieres
 const handlers = events[name]
 // 然后就是将handler添加进去events作为其属性,如果原来就有的话就改写成数组
 el.plain = false
}

代码生成(codegen)

上面通过编译得到ast节点之后,下一步就是根据节点信息生成代码字符串 也就是在genElement过程中,会通过genData生成节点的data对象

js
// compiler/codegen/index.js
function genData(el, state) {
  ...
  if (el.events) { 
  // 上面编译parse 时添加到el节点的events属性
  // data是最终返回的代码字符串
    data += `${genHandler(el.events, false)}`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el,nativeEvents, true)}`
  }
  // 上面两次函数调用差别在于第二个参数传入,标识是否为web原生事件
  ...
}

genHandlers 实现

js
// compiler/codegen/events.js
function genHandlers(events, isNative) {
  const prefix = isNative ? 'nativeOn:' : 'on'
  let staticHandlers = ''
  for (const name in events) {
    // 遍历每个event事件
    const handlerCode = genHandler(event[name])
    staticHandlers += `"${name}": ${handlerCode},`
  }
  staticHandlers = `{${staticHandlers.slice(0, -1)}}`
  return prefix + staticHandlers
}

function genHandler (handler) {
  if (!handler) { return 'function() {}' }
  if (Array.isArray(handler)) {
    return `[${handler.map(handler => genHandler(handler)).join(',')}]`
  }
  const isMethodPath = simplePathRE.test(handler.value) 
  // @click="handler" 正常写法
  const isFunctionExpression = fnExpRE.test(handler.value)
  // @click="function a(){} || () => {}" 函数声明式或者箭头函数
  const isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')
  // @click="handler($event)"
  if (!handler.modifiers) {
    // 没有修饰符
    if (isMethodPath || isFunctionExpression) {
      // 前两种写法,直接返回表达式
      return handler.value
    }
    // 第三种写法,需要在外面包一层函数声明,将$event参数出入
    return `function($event) {
      ${isFunctionInvocation ? `return ${handler.value}` : handler.value
    }`
  } else {
    // 包含修饰符
    let code = ''
    let genModifierCode = ''
    const keys = []
    for (const key in handler.modifiers) {
      if (modifierCode[key]){
        genModifierCode += modifierCode[key] // 不用修饰符添加不同代码
      } else if (key === 'exact') {
        const modifiers = handler.modifiers
        genModifierCode += genGuard(
          ['ctrl', 'shift', 'alt', 'meta']
            .filter(i => !modifiers[i])
            .map(i => `$event.${i}Key`
            .join('||')
      } else {
        keys.push(key)
      }
    }
    if (keys.length) { code += genKeyFilter(keys) }
    if (genModifierCode) code += genModifierCode
    // 这里是将修饰符的代码先合并到事件处理代码前半部
    const handlerCode = isMethodPath
      ? `return ${handler.value}($event)`
      : isFunctionExpression
        ? `return (${handler.value})($event)` // 做一个立即执行函数
        : isFunctionInvocation
          ? `return ${handler.value}`// 直接用就行,这里是第三种写法,已经自己传入$event
          : handler.value
    return `function($event){${code}${handlerCode}}`
    // 最终返回事件处理函数的字符串
  }
}

小结

上述生成的事件处理函数字符串,返回到generate 函数中返回的code,返回成vm实例的data对象,具体可参考渲染函数生成vue组件中的数据对象

事件绑定的时机

事件的绑定时机,发生在生成vnode之后的create钩子函数调用,也就是patch过程中invokeCreateHooks

总共有两个调用时机

  • createElm过程中,创建普通vnode节点之后
  • createComponent过程中,组件初始化initComponent过程中

updateDOMListeners

定义实现是对应不同平台的modules

js
// platforms/web/runtime/modules/events.js
function updateDOMListeners(oldVnode, vnode) {
  const on = vnode.data.on || {}
  const oldOn = oldVnode.data.on || {}
  // 这些是vnode节点里面的事件对象
  target = vnode.elm
  // 取出绑定元素
  updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
  // 添加事件
  target = undefined
}

updateListeners

js
// core/vdom/helpers/udpate-listeners.js
function updateListeners(on, oldOn, add, remove, createOnceHandler, vm) {
  let name, def, cur, old, event
  for (name in on) {
    def = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)
    // 这里是针对之前如果有特定修饰符,如once的,会在name的字符串前加~,这里将其解析回来
    if (isUndef(old) {
      // 没有oldvnode的事件,也就是新绑定事件
      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) {
      // 这里是组件更新,导致这些事件处理函数也更新的情况
      // 这里只更新了invoker的fns属性,因为最终执行时间处理的时候是取出该属性值进行计算,所以只需要修改属性值的指向即可
      old.fns = cur
      on[name] = old
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name]){
      // 组件更新,删去无用的事件处理
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

function createFnInvoker(fns, vm) {
  function invoker() {
    // 最终的事件处理逻辑,取出fns属性值进行执行
    const fns = invoker.fns
    if (Array.isArray(fns)) {
      for (let i = 0; i < fns.length; i++) {
        fns[i].apply(null, arguments)
      }
    } else {
      return fns.apply(null, arguments)
    }
  }
  invoker.fns = fns
  return invoker
  // 这里的invoker也就是最终返回到外面cur上的方法,也就是最终事件调用函数
  // 每次调用事件,就会将传入的fns取出,然后执行
}

add 实现

js
// platforms/web/runtime/modules/events.js
function add (name, handler, capture, passive) {
  // handler = cur
  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.onwerDocument !== document
        ) {
          return original.apply(this, arguments)
        }
    }
    // 对传入的目标元素绑定事件处理
    target.addEventListener(name, handler, supportPassive ? {capture, passive} : capture)
  }
}

这里会有个疑问:组件的原生事件是用nativeOn存储,但是上面取出绑定的是on属性 分析:在创建组件vnode的过程中发生过一次变量交换

js
// core/vdom/create-components.js
function createComponent() {
  ...
  const listener = data.on
  data.on = data.nativeOn
  // 所以组件自定义事件是用 listener 属性存储了
  const vnode = new VNode(
    `vue-component-${Ctor.cid}-${name}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  // listeners 最后是作为组件占位VNode的属性传入
}

自定义事件的实现

上面是对原生事件的绑定实现,下面则是对组件的自定义事件的实现

自定义事件是存储到listeners对象中,并存储到组件vnode的属性中 在创建组件vm实例的时候,会对组件事件进行初始化

js
// core/instance/init.js
Vue.prototype._init = function(options) {
  // 这里的options是在createComponentInstanceForVnode的时候传入
  if (options && options._isComponent) {
    initInternalComponent(vm, options)
    // component的合并配置
  }
  ...
  initEvents(vm)
}


function initInternalComponent(vm, options) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // 创建组件options
  ...
  opts._parentVnode = parentVnode
  // 这里是组件占位符vnode
  const vnodeComponentOptions = parentVnode.componentsOptions
  ...
  opts._parentListeners = vnodeComponentOptions.listeners
  // 这里是将组件vnode的listeners赋值给组件vm实例里的_parentListeners
  // 然后回到外面的initEvents
}

function initEvents(vm) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  const listeners = vm.$options._parentListeners
  if (listeners) {
    // 组件事件绑定
    updateComponentListeners(vm, listeners)
  }
}

function updateComponentListeners(vm, listeners, oldListeners) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = null
  // target 全局变量,存储当前vm
}

这里的updateListeners和web原生事件绑定时的updateListeners是同个函数,只是传入的方法不一样

自定义事件内部实现

js
function add(event, fn) {
  target.$on(event, fn)
}
function remove(event, fn) {
  target.$off(event, fn)
}
// 通过vm实例的$on/$off 进行事件的绑定和删除
Vue.prototype.$on = function(event, fn){
  const vm = this
  if (Array.isArray(event)) {
    // 传入的是列表event,循环调用$on
  } else {
    (vm._events[event] || (vm._events[event] = [])).push(fn)
    // 组件vm的自定义事件,就是将回调函数推入_events[event]的数组中
    // 调用的时候就循环调用该数组
    if (hookRE.test(event)) {
      // 可以用 hook:mounted 这样来定义一个钩子事件
      vm._hasHookEvent = true
    }
    return true
  }
}

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
  }

Vue.prototype.$off = function(event, fn) {
  const vm = this
  if (!arguments.length) {
    // 不传任何参数,就会删掉vm实例所有事件回调函数
    vm._events = Object.create(null)
    return vm
  }
  if (Array.isArray(event)) {
    // 循环调用$off
    return vm
  }
  const cbs = vm._events[event]
  if (!cbs) {
    return vm
  }
  if (!fn) {
    vm._events[event] = null
    return vm
    // 不传入fn参数,就直接删除该事件所有回调
  }
  let cb, i = cbs.length
  while (i--) {
    cb = cbs[i]
    if (cb === fn || cb.fn === fn) { 
      // 在回调数组中找到匹配的,然后删除
      cbs.splice(i, 1)
      break
    }
  }
  return vm
}

Vue.prototype.$emit = function(event) {
  const vm = this
  ...
  let cbs = vm._events[event]
  if (cbs) {
    cbs.length > 1 ? toArray(cbs) : cbs
    const args = toArray(arguments, 1)
    // 也就是$emit(event, args),这里的args是将emit的入参从下标1开始取,舍弃了event参数
    for (let i = 0, l = cbs.length; i < l; i++) {
      // 循环调用cbs回调
      try {
        cbs[i].apply(vm, args)
      } catch (e) {
        handleError(e, vm, `event handler for "${event}"`)
      }
    }
  }
}

总结

经典事件中心实现,利用vm._events存储所有的组件事件,然后vm内部触发本身定义的事件 但是,由于传入的回调函数是在父级组件定义,所以调用的时候就是调用父级组件的方法