Event Event Processing

If you think it is good, please send me a Star at GitHub

In the process of writing a Vue application, there must be an event event. Events in Vue allow us to handle native interactions such as Click and Mouse, as well as component communication.

In the event the event processing section, we first review the first event common use of events, and then combined with the compiling principle to analyze how events are parsed, immediately according to the DOM events and custom of the two different event types, to analyze respectively, finally we analyze the common events of Vue modifier is how to deal with.

Common use

The most common use of events is to bind an event name directly:

<div @click="handleClick"></div>
Copy the code

If you need to pass parameters, you can also bind a function call directly and pass the parameters during the function call:

<! -- Common parameters -->
<button @click="handleIncrementClick(10)">Increment</button>
<! -- Original DOM event parameter + normal parameter -->
<button @click="handleIncrementClick($event, 10)">Increment</button>
<! Arrow function -->
<button @click="() => handleIncrementClick(10)">Increment</button>
Copy the code

When you bind an event, you can also write event modifiers:

<! -- Component event modifier -->
<child-component @click.native="handleChildComponentClick" />
<! -- Native event modifier -->
<button @click.stop.prevent="handleIncrement">Increment</button>
<! Add modifier to event name -->
<div @click.stop"></div>
Copy the code

If you want to bind events dynamically, you can use the event dynamic parameter:

const eventName = 'click'
const template = '<button @[eventName]="handleIncrement">Increment</button>'
Copy the code

Parsing of events

For event parsing, we use the following code as an example to illustrate:

new Vue({
  el: '#app'.template: '<button @click="handleClick">Button</button>'.methods: {
    handleClick () {
      console.log('click handle')}}})Copy the code

Since V-ON event parsing is also a special instruction, the parse portion of the event is the same as the instruction. Let’s look directly at the AST results before calling the processAttrs method during parse:

const ast = {
  type: 1.tag: 'button'.attrsList: [{name: '@click'.value: 'handleClick'}].attrsMap: {
    '@click': 'handleClick'
  },
  rawAttsMap: {
    '@click': { name: '@click'.value: 'handleClick'}}}Copy the code

Next, call the processAttrs method we mentioned during instruction and filter parsing:

export const dirRE = process.env.VBIND_PROP_SHORTHAND ? /^v-|^@|^:|^\.|^#/ : /^v-|^@|^:|^#/
export const bindRE = /^:|^\.|^v-bind:/
export const onRE = /^@|^v-on:/
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
    if (dirRE.test(name)) {
      // mark element as dynamic
      el.hasBindings = true
      // modifiers omit code
      if (bindRE.test(name)) {
        // v-bind omits the code
      } else 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)
      } else {
        // Normal directives omit the code}}else {
      / /... Omit code}}}Copy the code

Because we are only analyzing event-related code in this chapter, we will focus only on the ELSE If branch V-ON.

In the else if branch, the onRE regular expression is called to match the name, and the addHandler method is called when the match is successful. The addHandler method is defined in the SRC/Compiler /helpers.js file with the following simplified code:

export function addHandler (el: ASTElement, name: string, value: string, modifiers: ? ASTModifiers, important? : boolean, warn? :?Function, range? : Range, dynamic? : boolean) {
  modifiers = modifiers || emptyObject
  / /... Omit code
  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
}
Copy the code

Code analysis:

  • First determine if the modifier existsnativeIf so, store the eventnativeEventsObject, otherwise stored ineventsIn the. aboutDOMEvents and custom events are analyzed separately in a later section, as long as they are stored in different locations.
  • At the end of the code, it decides firsthandlersIs an array, if so, according toimportantThe parameter value is selected to be added to the head or tail of the array, as inelse ifBranch, calladdHandlerMethod is passed as an argumentfalse, so it’s added to the end of the array; If it is not an array, it continues to determine whether there is already a value, and if there is, it is processed as an array. If it is neither an array nor a value, it simply assigns. The logic of this code means that we can listen for the same event repeatedly and bind different event names, which are not overwritten by each other, but are called sequentially when the event is triggeredeventsEach function in the array.
new Vue({
  el: '#app'.template: '<button @click="handleOneClick" @click="handleTwoClick">Button</button>'.methods: {
    handleOneClick () {
      console.log('one click handle')
    },
    handleTwoClick () {
      console.log('two click handle')}}})Copy the code

Note: While we can do this and it will work just fine with the code logic, listening for the same event repeatedly and providing different event names makes the code ambiguous. Because we do not know the source code, we will largely mistakenly believe that repeated monitored events will be covered by each other, so when Vue is in patch, it will detect and prompt error messages in the development environment:

function makeAttrsMap (attrs: Array<Object>) :Object {
  const map = {}
  for (let i = 0, l = attrs.length; i < l; i++) {
    if( process.env.NODE_ENV ! = ='production'&& map[attrs[i].name] && ! isIE && ! isEdge ) { warn('duplicate attribute: ' + attrs[i].name, attrs[i])
    }
    map[attrs[i].name] = attrs[i].value
  }
  return map
}

// An error message was reported
'Error compiling template: duplicate attribute: @click'
Copy the code

After the processAttrs method is called, the parse phase is almost over, and the AST object looks like this:

const ast = {
  type: 1.tag: 'button'.attrsList: [{name: '@click'.value: 'handleClick'}].attrsMap: {
    '@click': 'handleClick'
  },
  rawAttsMap: {
    '@click': { name: '@click'.value: 'handleClick'}},events: {
    click: { value: 'handleClick'.dynamic: false}}}Copy the code

Finally, let’s analyze the event generate:

const code = generate(ast, options)
Copy the code

When the AST is generated, a very core genData method is called when generate is called, with the following event-related code in this method:

export function genData (el: ASTElement, state: CodegenState) :string {
  let data = '{'
  / /... Omit code
  if (el.events) {
    data += `${genHandlers(el.events, false)}, `
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true)}, `
  }
  / /... Omit code
  return data
}
Copy the code

In the genData method, both DOM and custom events are handled by calling the genHandlers method, which looks like this:

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
  }
}
Copy the code

In analyzing the genHandlers approach, we illustrate it in two different dimensions

  • Dynamic and static event names: Differentiates dynamic event names from static event namesdynamicAttribute, suppose we have the following case:
// Static event name
const staticTemplate = '<button @click="handleClick">Button</button>'

// Dynamic event name
const eventName = 'click'
const dynamicTemplate = '<button @[eventName]="handleClick">Button</button>'
Copy the code

After calling the genHandlers method, the methods return the following values:

// Static event names return results
const staticResult = 'on:{"click":handleClick}'

// Dynamic event names return results
const dymamicResult = 'on:_d({},[eventName,handleClick])'
Copy the code

Note: The _d argument, like the _f and _s functions, is an abbreviated form of a function. The code is as follows:

export function bindDynamicKeys (baseObj: Object, values: Array<any>) :Object {
  for (let i = 0; i < values.length; i += 2) {
    const key = values[i]
    if (typeof key === 'string' && key) {
      baseObj[values[i]] = values[i + 1]}else if(process.env.NODE_ENV ! = ='production'&& key ! = =' '&& key ! = =null) {
      // null is a special value for explicitly removing a binding
      warn(
        `Invalid value for dynamic directive argument (expected string or null): ${key}`.this)}}return baseObj
}
Copy the code
  • Element tag DOM event types and component native DOM event types: Distinguishes element labelsDOMEvent type and component nativeDOMEvent type, the key point isnativeEvent modifier, suppose we have the following case:
// Element tag DOM event type
const template = '<button @click="handleClick">Button</button>'

// The component's native DOM event type
const nativeTemplate = '<child-component @click.native="handleClick" />'
Copy the code

After calling the genHandlers method, the methods return the following values:

// The element tag DOM event returns the result
const result = 'on:{"click":handleClick}'

// The component's native DOM event returns the result
const nativeResult = 'nativeOn:{"click":function($event){return handleClick($event)}}'
Copy the code

After analyzing the genHandlers method, let’s examine the genHandler method:

const simplePathRE = /^[A-Za-z_$][\w$]*(? :\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"] |\[\d+]|\[[A-Za-z_$][\w$]*])*$/
const fnExpRE = /^([\w$_]+|\([^)]*? \))\s*=>|^function(? :\s+[\w$]+)? \s*\(/
const fnInvokeRE = / \ [^)] *? \); * $/
function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>) :string {
  if(! handler) {return 'function(){}'
  }
  if (Array.isArray(handler)) {
    return ` [${handler.map(handler => genHandler(handler)).join(', ')}] `
  }
  const isMethodPath = simplePathRE.test(handler.value)
  const isFunctionExpression = fnExpRE.test(handler.value)
  const isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, ' '))
  if(! handler.modifiers) {if (isMethodPath || isFunctionExpression) {
      return handler.value
    }
    / /... Omit code
    return `function($event){${
      isFunctionInvocation ? `return ${handler.value}` : handler.value
    }} ` // inline statement
  } else {
    // omit decorator code}}Copy the code

Note: In the genHandler method, we omit the logic to handle the else branch of the modifier, which we will cover in a later section.

Code analysis:

  • Variable declaration:isMethodPathIndicates whether the access mode is simple,isFunctionExpressionRepresents whether the function expression mode,isFunctionInvocationRepresents whether it is a function call.
// Simple access mode
// a a.b a['b'] a["b"] a[b]
const eventMap = {
  clickName: 'handleClick'
}
const simpleWay1 = '<button @click="handleClick">Button</button>'
const simpleWay2 = '<button @click="eventMap.clickName">Button</button>'
const simpleWay3 = '<button @click="eventMap["clickName"]">Button</button>'
// ...

// Function expression
const funcExpression = '<button @click="() => handleClick()">Button</button>'

// Function call mode
const funcInvocation = '<button @click="handleClick()">Button</button>'
Copy the code
  • Simple access: Suppose we have the following case:
const simpleWay = '<button @click="handleClick">Button</button>'
Copy the code

Because it is easy to access, isMethodPath is true and returns handler.value. GenHandler and genHandlers return the following:

const genHandlerResult = 'handleClick'
const genHandlersResult = 'on:{"click":handleClick}'
Copy the code
  • Function expression: Suppose we have the following case:
const funcExpression = '<button @click="()=>handleClick()">Button</button>'
Copy the code

Because isFunctionExpression is true, handler.value is returned directly, and genHandler and genHandlers return the following result:

const genHandlerResult = '()=>handleClick()'
const genHandlersResult = 'on:{"click":()=>handleClick()}'
Copy the code
  • Function call: Suppose we have the following case:
const funcInvocation = '<button @click="handleClick()">Button</button>'
Copy the code

Because isMethodPath and isFunctionExpression are false, the if branch is not used. GenHandler and genHandlers return the following result:

const genHandlerResult = 'function($event){return handleClick()}'
const genHandlersResult = 'on:{"click":function($event){return handleClick()}}'
Copy the code
  • Repeat listener event mode: Suppose we have the following case:
const repeatTemplate = '<button @click="handleOneClick" @click="handleTwoClick">Button</button>'
Copy the code

When handler is an array, the genHandler method is recursively called, and the result of each function is concatenated to the array string as an array element. GenHandler and genHandlers return the following result:

const genHandlerResult = '[handleOneClick,handleTwoClick]'
const genHandlersResult = 'on:{"click":[handleOneClick,handleTwoClick]}'
Copy the code

After the analysis of genHandler method is completed, the codeGen code generation process is officially ended. Returning to our original example, the return value of the generate method is as follows:

const template = '<button @click="handleClick">Button</button>'

const code = generate(ast, options)
// code prints the result
{
  render: `with(this){return _c('button',{on:{"click":handleClick}},[_v("Button")])}`.staticRenderFns: []}Copy the code

DOM events and custom events

DOM events

Native DOM events need to be written on HTML element tags, and if you want to add native DOM events to component tags, you must provide native event modifiers.

In this chapter, we first analyze how DOM events are processed in the process of patch. We take the following code as an example to illustrate:

new Vue({
  el: '#app'.template: '<button @click="handleClick">Button</button>'.methods: {
    handleClick () {
      console.log('click handle')}}})Copy the code

Review the patch function assignment process, the file path to the SRC/platforms/web/runtime/patch. Js:

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
Copy the code

At the time of call createPatchFunction method, we have passed some platform related modules, these modules defined in SRC/platforms/web/runtime/modules directory below. Under these directories, we only care about the code for the event.js file:

export default {
  create: updateDOMListeners,
  update: updateDOMListeners
}
Copy the code

Similar to the directive, the updateDOMListeners are called automatically in the component’s create and Update hook functions. We’ll skip the timing of the call and go straight to the definition of updateDOMListeners:

let target
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
}
Copy the code

The updateDOMListeners method isn’t very coded and starts with the current VNode, the event listeners on the old VNode, and the current element tags. Event listening is then handled by calling normalizeEvents and updateListeners.

Since the normalizeEvents method is mainly used to deal with v-Models, let’s skip this and go straight to the code for the updateListeners method:

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 (__WEEX__ && isPlainObject(def)) {
      cur = def.handler
      event.params = def.params
    }
    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(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)
    }
  }
}
Copy the code

The code for the updateListeners method is long, but there’s nothing complicated about what you do.

  • for-intraverseonObject: its function is used toaddAdd event listeners or update event listeners. In this loop, it first determines whether the current event is defined, and if not, it prompts an error message in the development environment. If it is defined, but not in old event listening, it should be usedaddTo add this event listener; If both the current event listener and the old event listener exist, but are not the same. Indicates that the same event is being listened for, but the callback function is different and the event should be updated.
  • for-intraverseoldOnObject: Used to remove event listeners. In this loop, judging that the old event listeners are not in the new event listeners indicates that the event listeners should be removed and the event listener calls removedremoveMethods.

Add Adds event listening

Because updateListeners methods defined in SRC/core/vdom/helpers/update – listeners. Js file, it as a general method, the add and remove parameters are passed differ according to the external environment. In the native DOM events, add code defined in SRC/platforms/web/runtime/modules/event. The js file, the code is as follows:

function add (
  name: string,
  handler: Function,
  capture: boolean,
  passive: boolean
) {
  / /... Omit processing browser-compatible code
  target.addEventListener(
    name,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}
Copy the code

Remove Removes event listening

The remove and add methods are defined in the same place and have the following code:

function remove (
  name: string,
  handler: Function, capture: boolean, _target? : HTMLElement) {
  (_target || target).removeEventListener(
    name,
    handler._wrapper || handler,
    capture
  )
}
Copy the code

createFnInvoker

After introducing the add and remove methods, let’s look at one of the more important createFnInvoker methods:

export function createFnInvoker (fns: Function | Array<Function>, vm: ? Component) :Function {
  function invoker () {
    const fns = invoker.fns
    if (Array.isArray(fns)) {
      const cloned = fns.slice()
      for (let i = 0; i < cloned.length; i++) {
        invokeWithErrorHandling(cloned[i], null.arguments, vm, `v-on handler`)}}else {
      // return handler return value for single handlers
      return invokeWithErrorHandling(fns, null.arguments, vm, `v-on handler`)
    }
  }
  invoker.fns = fns
  return invoker
}
Copy the code

Code analysis: First define an Invoker method, then assign the current event listener for the for-in loop to the FNS property of the Invoker method, and then return invoker. This means that the handler argument to the subsequent call to the Add method is invoker, not directly the handleClick method in our example, which is wrapped in a layer.

button.addEventListener('click'.function invoker () {})Copy the code

When we click on the Button to trigger the click event, the invoker method starts executing. In this method, it first takes the FNS property, and then decides to iterate over and call its callback if it is an array, or call it if it is not an array. In other words, the FNS obtained here is the handleClick method in our case.

Note that the invokeWithErrorHandling method simply wraps a try/catch layer around the function call to make it easier to catch errors. Instead of considering exceptions, you can use invokeWithErrorHandling instead of fn. Apply (context, args).

export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if(res && ! res._isVue && isPromise(res) && ! res._handled) { res.catch(e= > handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true}}catch (e) {
    handleError(e, vm, info)
  }
  return res
}
Copy the code

Custom events

As mentioned earlier, only components can have both custom events and native DOM events. To better understand how components handle events, let’s write the following example:

Vue.component('child-component', {
  template: '<button @click="handleClick">Button</button>'.methods: {
    handleClick () {
      console.log('click handler')
      this.$emit('select')}}})new Vue({
  el: '#app'.template: '<child-component @select="handleSelect" @click.native="handleClick" />'.methods: {
    handleClick () {
      console.log('child native click handler')
    },
    handleSelect () {
      console.log('child customer select handler')}}})Copy the code

Before analyzing the patch component, let’s look at the render function generated on the child-component child:

const render = `with(this){ return _c('child-component',{ on:{ "select":handleSelect }, nativeOn:{ "click":function($event){ return handleClick($event) } } }) }`
Copy the code

SRC /core/vdom/create-component.js: SRC /core/vdom/create-component.js: SRC /core/vdom/create-component.js

export function createComponent (
  Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
  / /... Omit code
  const listeners = data.on
  data.on = data.nativeOn
  / /... Omit code
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
    data, undefined.undefined.undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  return vnode
}
Copy the code

In the createComponent method, there are two key pieces of code:

// Custom event assignment
const listeners = data.on

// Native DOM event assignment
data.on = data.nativeOn
Copy the code

As mentioned in the native DOM Event section, it takes data.on for native DOM events. For components, however, it appears on nativeOn objects rather than on objects because of the added native modifier. And because the on object is a component custom event that we wrote, it needs special handling.

After processing, the component’s native DOM event is handled in a manner previously mentioned and not described here, except that the target at which the event is added is the root node of the child node.

Now that the component’s native Click event is the same as before, let’s jump right into the processing of the Select custom event. When creating a component VNode using createComponent, the child component’s constructor is executed, followed by a call to the initInternalComponent method:

Vue.prototype._init = function (options? :Object) {
    const vm: Component = this
    / /... Omit code
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      / /... Omit code
    }
    / /... Omit code
    initEvents(vm)
    / /... Omit code}}Copy the code

The code for the initInternalComponent method is as follows:

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  / /... Omit code
  const vnodeComponentOptions = parentVnode.componentOptions
  opts._parentListeners = vnodeComponentOptions.listeners
  / /... Omit code
}
Copy the code

Note: here vnodeComponentOptions. Listeners is our select custom events.

After initInternalComponent completes execution, initEvents is called as follows:

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)
  }
}
Copy the code

The child listeners can retrieve the parent listeners via _parentListeners and call the updateComponentListeners method on initEvents. The code for this method is as follows:

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

The code for the updateComponentListeners method is simple, it first sets the target of the event listener to the current child component instance and then calls the updateListeners method. The updateListeners method is the same one mentioned in the native DOM events section. The difference is that the add method passed here is a little different than the remove method.

function add (event, fn) {
  target.$on(event, fn)
}
function remove (event, fn) {
  target.$off(event, fn)
}
Copy the code

$on and $off are two methods in Vue’s built-in event system, defined in eventMixin methods:

export function eventsMixin (Vue: Class<Component>) {
  Vue.prototype.$on = function () {}
  Vue.prototype.$once = function () {}
  Vue.prototype.$off = function () {}
  Vue.prototype.$emit = function () {}}Copy the code

The rationale for the above methods was covered in the Overall eventMixin process section, and will not be covered here.

Based on the above analysis, we know that the child component’s Button element adds two click native DOM events, and the child component’s VM instance adds a custom SELECT event. When we click the button, it prints as follows:

'click handler'
'child customer select handler'
'child native click handler'
Copy the code

Handling of common modifiers

In the directive chapter, we mentioned modifiers. The process is the same for event modifiers, assuming we have the following case:

const template = '<button @click.stop.prevent="handleClick">Button</button>'
Copy the code

When parsing into an AST, modifiers are generated as follows:

const modifiers = {
  stop: true.prevent: true
}
Copy the code

In the genHandler method, we omit the else branch to handle modifiers, which we’ll use in this section when analyzing event modifiers:

function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>) :string {
  / /... Omit code
  const isMethodPath = simplePathRE.test(handler.value)
  const isFunctionExpression = fnExpRE.test(handler.value)
  const isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, ' '))

  if(! handler.modifiers) {/ /... Omit code
  } else {
    let code = ' '
    let genModifierCode = ' '
    const keys = []
    for (const key in handler.modifiers) {
      if (modifierCode[key]) {
        genModifierCode += modifierCode[key]
        // left/right
        if (keyCodes[key]) {
          keys.push(key)
        }
      } else if (key === 'exact') {
        / /... Omit the case modifier
      } else {
        keys.push(key)
      }
    }
    if (keys.length) {
      code += genKeyFilter(keys)
    }
    // Make sure modifiers like prevent and stop get executed after key filtering
    if (genModifierCode) {
      code += genModifierCode
    }
    const handlerCode = isMethodPath
      ? `return ${handler.value}($event)`
      : isFunctionExpression
        ? `return (${handler.value})($event)`
        : isFunctionInvocation
          ? `return ${handler.value}`
          : handler.value
    / /... Omit code
    return `function($event){${code}${handlerCode}} `}}Copy the code

Native modifier

As for the native modifier, we more or less mentioned its use earlier, so let’s focus on it now.

In the parse phase, events objects or nativeEvents objects are created based on whether there are native modifiers:

if (modifiers.native) {
  delete modifiers.native
  events = el.nativeEvents || (el.nativeEvents = {})
} else {
  events = el.events || (el.events = {})
}
Copy the code

Here is a template example and the AST object it parses to produce:

const tempalte = '<child-component @click.native="handleClick" />'
const ast = {
  type: 1.tag: 'child-component'.nativeEvents: {
    click: { value: 'handleClick'.modifiers: {}}}}Copy the code

In the CodeGen phase, the genHandlers method is called based on the presence of nativeEvents and Events as follows:

if (el.events) {
  data += `${genHandlers(el.events, false)}, `
}
if (el.nativeEvents) {
  data += `${genHandlers(el.nativeEvents, true)}, `
}
Copy the code

Here is a template example and the result returned by calling the genHandlers method:

const template = '<button @click="handleClick">Button</button>'
const nativeTemplate = '<child-component @click.native="handleClick" />'

const result = 'on:{click:"handleClick"}'
const nativeResult = 'nativeOn:{click:function($event){return handleClick($event)'
Copy the code

Stop, prevent, and self modifiers

Before we examine these modifiers, let’s look at an object:

const genGuard = condition= > `if(${condition})return null; `
const modifierCode: { [key: string]: string } = {
  stop: '$event.stopPropagation(); '.prevent: '$event.preventDefault(); '.self: genGuard(`$event.target ! == $event.currentTarget`),}Copy the code

Suppose we have the following case:

const template = '<button @click.stop.prevent.self="handleClick">Button</button>'
Copy the code

After parse completes, its AST objects look like this:

const ast = {
  type: 1.tag: 'button'.events: {
    click: {
      value: 'handleClick'.modifiers: { stop: true.prevent: true.self: true}}}}Copy the code

When the genHandler method is called, and the modiFIERS are an object, else branch logic is used, let’s look at the key code after this branch:

let code = ' '
let genModifierCode = ' '
for (const key in handler.modifiers) {
  if (modifierCode[key]) {
    genModifierCode += modifierCode[key]
  }
}
if (genModifierCode) {
  code += genModifierCode
}
const handlerCode = isMethodPath
  ? `return ${handler.value}($event)`
  : isFunctionExpression
    ? `return (${handler.value})($event)`
    : isFunctionInvocation
      ? `return ${handler.value}`
      : handler.value
return `function($event){${code}${handlerCode}} `
Copy the code

When iterating through the Modifiers, since the stop, prevent, and self we added are defined in the modifierCode object, the genModifierCode value looks like this after iterating through:

const genModifierCode = ` $event.stopPropagation(); $event.preventDefault(); if($event.target ! == $event.currentTarget)return null; `
Copy the code

Next, handlerCode is generated, which for our example is in simple access mode and isMethodPath is true, so handlerCode looks like this:

const handlerCode = `return handleClick($event)`
Copy the code

Finally, we need to combine all the codes, and the result is as follows:

const result = ` function($event){ $event.stopPropagation(); $event.preventDefault(); if($event.target ! == $event.currentTarget) return null; return handleClick($event) } `
Copy the code

When you see the results of reult, you will be able to understand this statement on Vue’s website: When using modifiers, order matters; The corresponding code is generated in the same order. Therefore, @click.prevent. Self blocks all clicks, whereas @click.self. Prevent only blocks clicks on the element itself.

Once the modifier

When analyzing the once modifier, we use the following example:

const template = '<button @click.once="handleClick">Button</button>'
Copy the code

If the once event modifier is provided at parse time, it will be handled in the addHandler method as follows:

if (modifiers.once) {
  delete modifiers.once
  name = prependModifierMarker('~', name, dynamic)
}
Copy the code

At the end of the Parse compilation phase, the ast objects generated are as follows:

// click faces a "~" symbol
const ast = {
  type: 1.tag: 'button'.events: {
    '~click': {
      value: 'handleClick'.modifiers: {}}}}Copy the code

After the codeGen code generation phase is complete, the generated render function is as follows:

const render = `with(this){return _c('button',{on:{"~click":function($event){return handleClick($event)}}},[_v("Button")])}`
Copy the code

When patch generates a VNode, the updateListeners method is called, where it handles the logic associated with the once event modifier as follows:

// Once = name.charat (0) === '~'
event = normalizeEvent(name)
if (isTrue(event.once)) {
  cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
Copy the code

Because updateListeners is a common method, createOnceHandler function parameter was passed by the external environment, it is defined in SRC/platforms/web/runtime/modules/events. The js file:

function createOnceHandler (event, handler, capture) {
  const _target = target // save current target element in closure
  return function onceHandler () {
    const res = handler.apply(null.arguments)
    if(res ! = =null) {
      remove(event, onceHandler, capture, _target)
    }
  }
}
function remove (
  name: string,
  handler: Function, capture: boolean, _target? : HTMLElement) {
  (_target || target).removeEventListener(
    name,
 
Copy the code

As you can see, in the createOnceHandler method, it returns an onceHandler method that, when we click the button, will use remove to remove the event listener, which is really why the once event modifier comes into play.

summary

We started by reviewing the various ways events can be used: normal mode, function expression mode, function call mode, use of event modifiers, and so on.

Then we also analyzed the event parsing process, know in different scenarios, the final generation of different render function.

Next, we analyze the processing of native DOM events and custom events in detail. We also know that only components can have both custom events and native DOM events by adding native event modifiers to the corresponding events. Native DOM events rely on addEventListener and removeEventListener, while custom events rely on the $ON and $off methods of their own event system.

Finally, we analyze the implementation principle of common event modifiers such as Native, Stop, prevent, self and once.

If you think it is good, please send me a Star at GitHub