current position:Home>Interpretation of vue source code 22: Do you know $event in vue?

Interpretation of vue source code 22: Do you know $event in vue?

2022-08-06 19:34:19small p

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情

本文主要内容摘抄自黄轶老师的慕课网课程Vue.js 源码全方位深入解析 全面深入理解Vue实现原理,主要用于个人学习和复习,不用作其他用途.

Our development work at ordinary times,处理组件间的通讯,The native interaction,Cannot leave the event.For a component element,We not only can bind the native DOM 事件,还可以绑定自定义事件,非常灵活和方便.From our website under reviewvueIn the event is how to use the.

事件处理

基本使用

<div id="example-2">
    <button v-on:click="greet">Greet</button>
</div>

var example2 = new Vue({
  el: '#example-2',
  data: {
    name: 'Vue.js'
  },
  // 在 `methods` 对象中定义方法
  methods: {
    greet: function (event) {
      // `this` 在方法里指向当前 Vue 实例
      alert('Hello ' + this.name + '!')
      // `event` 是原生 DOM 事件
      if (event) {
        alert(event.target.tagName)
      }
    }
  }
})
复制代码

除了直接绑定到一个方法,也可以在内联 JavaScript 语句中调用方法:

<div id="example-3">
  <button v-on:click="say('hi')">Say hi</button>
  <button v-on:click="say('what')">Say what</button>
</div>
复制代码

有时也需要在内联语句处理器中访问原始的 DOM 事件.可以用特殊变量 $event 把它传入方法:

<button v-on:click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>

methods: {
  warn: function (message, event) {
    // 现在我们可以访问原生事件对象
    if (event) {
      event.preventDefault()
    }
    alert(message)
  }
}
复制代码

事件修饰符

在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求.尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节.

为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符.之前提过,修饰符是由点开头的指令后缀来表示的.

<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>

<!-- 点击事件将只会触发一次 -->
// 不像其它只能对原生的 DOM 事件起作用的修饰符,`.once` 修饰符还能被用到自定义的[组件事件]
<a v-on:click.once="doThis"></a>
复制代码

按键修饰符

在监听键盘事件时,我们经常需要检查详细的按键.Vue 允许为 v-on 在监听键盘事件时添加按键修饰符:

<!-- 只有在 `key``Enter` 时调用 `vm.submit()` -->
<input v-on:keyup.enter="submit"> 复制代码

你可以直接将 KeyboardEvent.key 暴露的任意有效按键名转换为 kebab-case 来作为修饰符.

<input v-on:keyup.page-down="onPageDown">
复制代码

在上述示例中,处理函数只会在 $event.key 等于 PageDown 时被调用.

系统修饰键

可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器.

  • .ctrl
  • .alt
  • .shift
  • .meta

注意:在 Mac 系统键盘上,meta 对应 command 键 (⌘).在 Windows 系统键盘 meta 对应 Windows 徽标键 (⊞).

编译

Introduce the basic usage of event,Then we see to it from the perspective of source the principle.

为了更加直观,We use an example to analyze its implementation:

let Child = {
  template: '<button @click="clickHandler($event)">' +
  'click me' +
  '</button>',
  methods: {
    clickHandler(e) {
      console.log('Button clicked!', e)
      this.$emit('select')
    }
  }
}

let vm = new Vue({
  el: '#app',
  template: '<div>' +
  '<child @select="selectHandler" @click.native.prevent="clickHandler"></child>' +
  '</div>',
  methods: {
    clickHandler() {
      console.log('Child clicked!')
    },
    selectHandler() {
      console.log('Child select!')
    }
  },
  components: {
    Child
  }
})
复制代码

From the first compilation phase began to see the,在 parse 阶段,When closing tag will performcloseElement,This function is performed againprocessElement,It performs aprocessAttrs

function closeElement (element) {
    if (!inVPre && !element.processed) {
      element = processElement(element, options)
    }
}

function processElement (
  element: ASTElement,
  options: CompilerOptions
) {
  ...
  processRef(element)
  processSlotContent(element)
  processSlotOutlet(element)
  processComponent(element)
  ...
  processAttrs(element)
  return element
}
复制代码
const dirRE = /^v-|^@|^:|^\.|^#/
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)) {
      // 如果是 v- : @ At the beginning of responsive data is bound in
      el.hasBindings = true
      // 是否有修饰符,比如@click.stop stop就是修饰符
      modifiers = parseModifiers(name.replace(dirRE, ''))
      
      ...
      // Mainly for the event @click v-on:click
      if (onRE.test(name)) { // v-on
        name = name.replace(onRE, '')
        ...
        addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
      } 
    }
  }
}

const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g
function parseModifiers (name: string): Object | void {
  const match = name.match(modifierRE)
  if (match) {
    const ret = {}
    match.forEach(m => { ret[m.slice(1)] = true })
    return ret
  }
}
复制代码

In the process of the tag attributes,Determine if are instructions,首先通过 parseModifiers Resolve the modifier,Then determine if the event's instruction,则执行 addHandler(el, name, value, modifiers, false, warn) 方法,它的定义在 src/compiler/helpers.js 中:

export function addHandler (
  el: ASTElement,
  name: string,
  value: string,
  modifiers: ?ASTModifiers,
  important?: boolean,
  warn?: ?Function,
  range?: Range,
  dynamic?: boolean
) {
  modifiers = modifiers || emptyObject
 
  // right middleThis is for the mouse right click 和 The key processing
  // You can see if the modifier isright,The actual call event iscontextmenu
  // 如果修饰符是middle,The actual call event ismouseup
  if (modifiers.right) {
    if (name === 'click') {
      name = 'contextmenu'
      delete modifiers.right
    }
  } else if (modifiers.middle) {
    if (name === 'click') {
      name = 'mouseup'
    }
  }

  // Aiming at a series of processing modifier
  if (modifiers.capture) {
    delete modifiers.capture
    name = prependModifierMarker('!', name, dynamic)
  }
  if (modifiers.once) {
    delete modifiers.once
    name = prependModifierMarker('~', name, dynamic)
  }
  if (modifiers.passive) {
    delete modifiers.passive
    name = prependModifierMarker('&', name, dynamic)
  }
  
  let events
  // If the modifier hasnative,那么在el上挂载nativeEvents,Otherwise the mountevents
  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]
  // 给events赋值
  // Because of the same event can assign multiple,所以是一个数组,Such as the user can write two [email protected]="handle1" @click="handle2"
  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
}
复制代码

addHandler Function looks long,Actually did 3 件事情,首先根据 modifier 修饰符对事件名 name 做处理,接着根据 modifier.native Judgment is a pure native or ordinary events,分别对应 el.nativeEvents 和 el.events,最后按照 name Do classification of events,And hold the string of the callback function to the corresponding event.

在我们的例子中,父组件的 child 节点生成的 el.events 和 el.nativeEvents 如下:

el.events = {
  select: {
    value: 'selectHandler'
  }
}

el.nativeEvents = {
  click: {
    value: 'clickHandler',
    modifiers: {
      prevent: true
    }
  }
}
复制代码

image.png

子组件的 button 节点生成的 el.events 如下:

el.events = {
  click: {
    value: 'clickHandler($event)'
  }
}
复制代码

然后在 codegen 的阶段,会在 genData 函数中根据 AST Element nodes on the events 和 nativeEvents 生成 data 数据,它的定义在 src/compiler/codegen/index.js 中:

export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  // ...
  if (el.events) {
    data += `${genHandlers(el.events, false, state.warn)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true, state.warn)},`
  }
  // ...
  return data
}
复制代码

对于这两个属性,会调用 genHandlers 函数,定义在 src/compiler/codegen/events.js 中:

export function genHandlers (
  events: ASTElementHandlers,
  isNative: boolean
): string {
  const prefix = isNative ? 'nativeOn:' : 'on:'
  let staticHandlers = ``
 
  for (const name in events) {
    const handlerCode = genHandler(events[name])
    ...
    staticHandlers += `"${name}":${handlerCode},`
  }
  staticHandlers = `{${staticHandlers.slice(0, -1)}}`
  
}
复制代码
const modifierCode: { [key: string]: string } = {
  stop: '$event.stopPropagation();',
  prevent: '$event.preventDefault();',
  self: genGuard(`$event.target !== $event.currentTarget`),
  ctrl: genGuard(`!$event.ctrlKey`),
  shift: genGuard(`!$event.shiftKey`),
  alt: genGuard(`!$event.altKey`),
  meta: genGuard(`!$event.metaKey`),
  left: genGuard(`'button' in $event && $event.button !== 0`),
  middle: genGuard(`'button' in $event && $event.button !== 1`),
  right: genGuard(`'button' in $event && $event.button !== 2`)
}

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) {
    // 这种情况是@click="clickHandler"
    if (isMethodPath || isFunctionExpression) {
      return handler.value
    }
    // 这种情况是@click="clickHandler($event)"
    return `function($event){${ isFunctionInvocation ? `return ${handler.value}` : handler.value }}` // inline statement
  } else {
    let code = ''
    let genModifierCode = ''
    const keys = []
    for (const key in handler.modifiers) {
      if (modifierCode[key]) {
        genModifierCode += modifierCode[key]
      }
      ...
    }
   
    if (genModifierCode) {
      code += genModifierCode
    }
    const handlerCode = isMethodPath
      ? `return ${handler.value}.apply(null, arguments)`
      : isFunctionExpression
        ? `return (${handler.value}).apply(null, arguments)`
        : isFunctionInvocation
          ? `return ${handler.value}`
          : handler.value
    
    return `function($event){${code}${handlerCode}}`
  }
}
复制代码

genHandlers Methods traverse event object events,Calls to the same name of events genHandler(name, events[name]) 方法,Its content looks more,But in fact, the logic is simple,首先先判断如果 handler 是一个数组,Traverse it then the recursive call genHandler Methods and joining together the results,然后判断 hanlder.value Is a function invocation path or a function expression, 接着对 modifiers 做判断,对于没有 modifiers 的情况,就根据 handler.value 不同情况处理,要么直接返回,Either return a function expression of parcel;对于有 modifiers 的情况,则对各种不同的 modifer 情况做不同处理,添加相应的代码串.

So for our example,The parent component generated data 串为:

{
  on: {"select": selectHandler},
  nativeOn: {"click": function($event) {
      $event.preventDefault();
      return clickHandler($event)
    }
  }
}
复制代码

Child components generated data 串为:

{
  on: {"click": function($event) {
      clickHandler($event)
    }
  }
}
复制代码

那么到这里,Compile finished part,Let's take a look at runtime part is how to realize the.其实 Vue 的事件有 2 种,一种是原生 DOM 事件,A is a user-defined event,我们分别来看.

DOM 事件

还记得我们之前在 patch Perform a variety of module A hook function,At that time, this part is over,We have analyzed just before DOM 是如何渲染的,而 DOM Elements related attributes、样式、Events are through these module A hook function complete set of.

所有和 web 相关的 module 都定义在 src/platforms/web/runtime/modules 目录下,

image.png

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 })
复制代码

baseModules是在core/vdom/modules/index中,它包含refdirectives

import directives from './directives'
import ref from './ref'

export default [
  ref,
  directives
]
复制代码

在执行patch的时候,First will collect thesemodules:

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  const { modules, nodeOps } = backend

  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  ...
}
复制代码

That when performing thesehooks呢?我们来看下createElm的执行逻辑,首先创建DOM的真实节点,创建完成后,如果vnode.data存在,那么就执行invokeCreateHooks

function createElm () {
    ...
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    ...
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    
    if (isDef(tag)) {
        vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
        
        setScope(vnode)
        ...
        createChildren(vnode, children, insertedVnodeQueue)
        // 如果存在data,即属性,那么就执行invokeCreateHooks
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
        ...
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
}
复制代码

There is also a place also performinvokeCreateHooks,Is in the process of creating components when creating the component within the realDOM后,执行initComponent,It performs ainvokeCreateHooks

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
  
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

function initComponent (vnode, insertedVnodeQueue) {
    ...
    vnode.elm = vnode.componentInstance.$el
    if (isPatchable(vnode)) {
      // 此时vnodeFor a component of placeholder fu point
      invokeCreateHooks(vnode, insertedVnodeQueue)
      setScope(vnode)
    } else {
      ...
      insertedVnodeQueue.push(vnode)
    }
  }
复制代码

下面我们来看看invokeCreateHooks里面的逻辑:

function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  ...
}
复制代码

除了在createPhase calls thesehooks,在updatePhase also call

function patchVode() {
    ...
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    ...
}
复制代码

Figure it out when it's time to performhooks,So now take a look at thesehookds的逻辑,Here only focus on directory events.js 即可:

// event.js

let target: any
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 || oldVnode.elm
  normalizeEvents(on)
  updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
  target = undefined
}

export default {
  create: updateDOMListeners,
  update: updateDOMListeners,
  destroy: (vnode: VNodeWithData) => updateDOMListeners(vnode, emptyNode)
}
复制代码

首先获取 vnode.data.on,This is our previous generation data 中对应的事件对象,target 是当前 vnode 对应的 真实 DOM 对象,normalizeEvents 主要是对 v-model 相关的处理,我们之后分析 v-model 的时候会介绍,接着调用 updateListeners(on, oldOn, add, remove, vnode.context) 方法,它的定义在 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)
    ...
    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)
    }
  }
}

复制代码

updateListeners 的逻辑很简单,遍历 on To add events to monitor,遍历 oldOn To remove the event listeners,About listening and removal methods are external incoming,Because it was dealing with native DOM 事件的添加删除,Also deals with the custom events to add delete.

对于第一次,满足 isUndef(old) 并且 isUndef(cur.fns),会执行 cur = on[name] = createFnInvoker(cur) Method to create a callback function,然后在执行 add(event.name, cur, event.once, event.capture, event.passive, event.params) Complete an event.我们先看一下 createFnInvoker 的实现:

export function createFnInvoker (fns: Function | Array<Function>): Function {
  function invoker () {
    const fns = invoker.fns
    if (Array.isArray(fns)) {
      const cloned = fns.slice()
      for (let i = 0; i < cloned.length; i++) {
        cloned[i].apply(null, arguments)
      }
    } else {
      return fns.apply(null, arguments)
    }
  }
  invoker.fns = fns
  return invoker
}
复制代码

这里定义了 invoker 方法并返回,Due to an event may correspond to multiple callback function,So to make the array judgement here,Multiple callback function calls in turn.Note the final assignment logic, invoker.fns = fns,每一次执行 invoker 函数都是从 invoker.fns In execution of the callback function,回到 updateListeners,When we the second performs the function,判断如果 cur !== old,那么只需要更改 old.fns = cur The binding before involer.fns Assignment for the new callback function can be,并且 通过 on[name] = old Keep references,Thus ensure the event callback to add only one,Just modify it after the callback function reference.

So clever,Because this code below:

cur = on[name] = createOnceHandler(event.name, cur, event.capture)
复制代码

createOnceHandler返回invoker函数后,赋值给了on[name],The next time to update,old = oldOn[name],old就是invoker函数,所以我们只需要old.fns = cur即可.

updateListeners The last traversal functions oldOn 拿到事件名称,Determine if meet isUndef(on[name]),则执行 remove(event.name, oldOn[name], event.capture) To remove the event callback.

了解了 updateListeners 的实现后,Let's take a look at the original DOM Really adds callbacks and remove event callback function realization,它们的定义都在 src/platforms/web/runtime/modules/event.js 中:

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

function remove (
  name: string,
  handler: Function,
  capture: boolean,
  _target?: HTMLElement
) {
  (_target || target).removeEventListener(
    name,
    handler._wrapper || handler,
    capture
  )
}
复制代码

add 和 remove 的逻辑很简单,Is actually call native addEventListener 和 removeEventListener,And according to the parameter passing some configuration.

Below to see how events executed only once done?It is the event with a layer of parcel,Executed after the event at a timeremoveRemoved the event,This completes the executed only once.

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)
    }
  }
}
复制代码

The following example to step through the primary event is bound to,Here only consider native events,Don't consider the custom event.

const Child = {
  template: '<button @click="clickHandler($event)">' + 'click me' + '</button>',
  methods: {
    clickHandler(e) {
      console.log('Button clicked!', e)
      this.$emit('select')
    }
  }
}

new Vue({
  el: '#app',
  template:
    '<div>' +
    '<child @select="selectHandler" @click.native.prevent="clickHandler"></child>' +
    '</div>',
  components: {
    Child
  },
  methods: {
    clickHandler() {
      console.log('Child clicked!')
    },
    selectHandler() {
      console.log('Child select!')
    }
  }
})
复制代码

By compiling step-father components generated data 串为:

{
  on: {"select": selectHandler},
  nativeOn: {"click": function($event) {
      $event.preventDefault();
      return clickHandler($event)
    }
  }
}
复制代码

Child components generated data 串为:

{
  on: {"click": function($event) {
      clickHandler($event)
    }
  }
}
复制代码

根据上面的分析,All realDOMAfter the completion of the node generating,开始执行invokeCreateHooks,So the event bindings and son father first order,The component first sobutton进行事件绑定,When the component within the binding of events after the end of,For component placeholdervnode的事件绑定,此时target$1Is still a component within thebutton节点,所以button绑定了两个click事件,Order is the component of events within the priority,Placeholder fu points after the execution.

image.png

At the time of binding native events,我们都是从vnode.data.on上获取的,But the component was a native innativeon上,这是怎么回事呢?

function updateDOMListeners() {
  ...
  const on = vnode.data.on || {}
  const oldOn = oldVnode.data.on || {}
  ...
}
复制代码

Generate the component in our placeholder fu point when there are so a logicdata.on = data.nativeOn;

export function createComponent() {
  ...
  const baseCtor = context.$options._base;

  ...
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }
 
  ...
  data = data || {};
  // Assigned to the custom eventslisteners
  const listeners = data.on;
  // 把nativeOn指给on
  data.on = data.nativeOn;

  ...
  const name = Ctor.options.name || tag;
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    // Put your custom event here
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  );

  return vnode;
}
复制代码

自定义事件

除了原生 DOM 事件,Vue Also supports custom events,And the custom events only effects on the component,If use native events on component,需要加 .native 修饰符,Common elements used on .native 修饰符无效,Next we have to analyze its implementation.

在 render 阶段,If it is a component node,则通过 createComponent 创建一个组件 vnode,We'll review this method,定义在 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 {
  // ...
  const listeners = data.on
  
  data.on = data.nativeOn
  
  // ...
  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
}
复制代码

We focus on event correlation logic,可以看到,它把 data.on 赋值给了 listeners,把 data.nativeOn 赋值给了 data.on,So all the native DOM The event processing in the same way as we have just introduced,它是在当前组件环境中处理的.And for the custom event,我们把 listeners 作为 vnode 的 componentOptions 传入,It is in the stage of the subcomponents initialization processing,So its processing environment is child components.

怎么理解呢?

子组件child,We are dealing with native event binding inside the child components processing.当在组件childAlso binding on a@click.nativeNative events,此时是在app.vueComponents in processing,Within this componentchildIs the corresponding placeholder fu point,So the meaning of those words is native events within their respective components processing.The custom event is bound inchild身上,The components of the environment is at this timeapp.vue,Under the environment of this component will not deal with,只有在childEnvironment will handle the custom event.(比较绕)

And then at the time of initialization of child components,会执行 initInternalComponent 方法,它的定义在 src/core/instance/init.js 中:

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // ....
  const vnodeComponentOptions = parentVnode.componentOptions
 
  opts._parentListeners = vnodeComponentOptions.listeners
  // ...
}
复制代码

Here got the parent component of the incoming listeners,然后在执行 initEvents 的过程中,会处理这个 listeners,定义在 src/core/instance/events.js 中:

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)
  }
}
复制代码

拿到 listeners 后,执行 updateComponentListeners(vm, listeners) 方法:

let target: any
export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, vm)
  target = undefined
}
复制代码

updateListeners 我们之前介绍过,So for the custom events and native DOM Event handling differences in the event the realization of add and delete,Take a look at the custom event add 和 remove 的实现:

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

function remove(event, fn) {
  target.$off(event, fn);
}
复制代码

实际上是利用 Vue Define the events center,简单分析一下它的实现:

export function eventsMixin(Vue: Class<Component>) {
  const hookRE = /^hook:/;
  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[event] || (vm._events[event] = [])).push(fn);
      ...
    }
    return vm;
  };

  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?: string | Array<string>, fn?: Function ): Component {
    const vm: Component = this;
    // all
    if (!arguments.length) {
      vm._events = Object.create(null);
      return vm;
    }
    // array of events
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn);
      }
      return vm;
    }
    // specific event
    const cbs = vm._events[event];
    if (!cbs) {
      return vm;
    }
    if (!fn) {
      vm._events[event] = null;
      return vm;
    }
    // specific handler
    let cb;
    let 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: string): Component {
    const vm: Component = this;
    if (process.env.NODE_ENV !== "production") {
      const lowerCaseEvent = event.toLowerCase();
      ...
    }
    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;
  };
}
复制代码

The realization of the classic events center,All the events with vm._events 存储起来,当执行 vm.$on(event,fn) 的时候,According to the name of the event event 把回调函数 fn 存储起来 vm._events[event].push(fn).

当执行 vm.$emit(event) 的时候,根据事件名 event Find all the callback function let cbs = vm._events[event],Then traverse to perform all of the callback function.

当执行 vm.$off(event,fn) Will remove the specified event name event 和指定的 fn 当执行 vm.$once(event,fn) 的时候,内部就是执行 vm.$on,And when the callback function performs a through again after vm.$off Remove the callback event,Thus to ensure that the callback function is executed only once.

So for user-defined events add and remove is the center of the use of these events API.Need to be aware of things a bit,vm.$emit 是给当前的 vm On the event provided by the,The reason we commonly used it do parent-child communication component,Because of its callback function is defined in the parent component,When building component constructor to put back incomponentOptions中,This is passed to the child components$options.对于我们这个例子而言,当子组件的 button 被点击了,它通过 this.$emit('select') 派发事件,The child instance of a component are listening to this select 事件,The callback function and execute it——定义在父组件中的 selectHandler 方法,This is equivalent to finished a father and son components of communication.

总结

So at this point we are Vue Event implementation have further understanding,Vue 支持 2 种事件类型,原生 DOM Events and custom,They are the main difference is that different way add and delete events,And the custom events to the current instance is begin,But can take advantage of the components in the parent environment define the callback function to implement the father and son components of communication.

另外要注意一点,Only component node can add custom events,And add native DOM 事件需要使用 native 修饰符;And common element to use .native The modifier is useless,Also can add native DOM 事件.

copyright notice
author[small p],Please bring the original link to reprint, thank you.
https://en.cdmana.com/2022/218/202208061924591771.html

Random recommended