事件机制

一、事件类型

1、原生DOM事件

  • 普通DOM元素或组件.native修饰的事件;
  • patch调用createElm创建元素时利用target.addEventListener()添加事件

2、自定义事件

  • 组件自定义事件
  • _init调用initEvents初始化事件时,调用 Vue 原型上的$on将父组件绑定的自定义事件绑定到实例的_events属性上,并在方法中使用$emit触发该事件

二、源码解析

  • 事件在模板编译阶段以属性on的形式存在,在真实节点渲染阶段会根据事件属性去绑定相关的事件
  • 原生事件:createElm时调用create hook调用钩子函数updateDomListeners 传递的 add 调用addEventListener添加事件
  • 自定义事件:initEvents传递父组件绑定事件,updateListeners时调用传递的 add 内部的$on添加事件
  • 在解析到render函数后,事件都在on上

1、原生事件

patch执行createElm时,调用updateDomListeners

// 调用createChildren遍历子节点创建对应的DOM节点
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
  // 执行create钩子函数(directives events等的create hook)
  // updateAttrs
  // updateClass
  // updateDomListeners 普通的原生事件
  // updateDomProps
  // updateStyle
  // updateDirectives
  invokeCreateHooks(vnode, insertedVnodeQueue)
}

updateDomListenerssrc/platforms/web/runtime/patch.js创建平台相关patch时引入

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'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

// 引入基础modules和平台相关modules
// 基础directives ref
export const patch: Function = createPatchFunction({ nodeOps, modules })

createPatchFunction创建patch时在cbs中保存所以modules的hook

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]])
    }
  }
}

updateDOMListeners取出新旧dom节点的on事件,并兼任处理IE下不支持input事件,修改为change事件后,调用updateListeners

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
}

遍历on事件对新节点事件绑定注册事件,对旧节点移除事件监听;
add函数调用原生的addEventListener在真正的 DOM上绑定事件。

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)
    }
  }
}
target.addEventListener(
  name,
  handler,
  supportsPassive
    ? { capture, passive }
    : capture
)

2、自定义事件

  • 子组件通过vm.$emit向父组件派发事件,父组件通过v-on:(event)绑定的事件方法接受信息并处理回调;
    initInternalComponent时,保存上级组件传递的数据:
    options._parentVnode是在调用createComponentInstanceForVnode创建组件实例时保存的父级组件vnode
    options.parent是在调用createComponentInstanceForVnode创建组件实例时保存的父级组件实例
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
  }
}

_init方法中调用initEvents

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')

src/core/instance/events.js
清空数据,初始化连接父级事件;
创建私有的事件对象和是否有事件钩子的标志两个属性,
然后根据父级是否有事件处理器来决定是否更新当前实例的事件监听器

vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
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
}

只传递了on和vm,代码简化后,add方法调用的$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) {
    def = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)
     ...
      add(event.name, cur, event.capture, event.passive, event.params)
     ...
  }
}

event是数组则遍历执行$on;
否则在initEvents时声明的_events对象的该事件数组中添加该事件

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)
    // optimize hook:event cost by using a boolean flag marked at registration
    // instead of a hash lookup
    if (hookRE.test(event)) {
      vm._hasHookEvent = true
    }
  }
  return vm
}

自定义事件触发$emit

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
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

相关阅读更多精彩内容

友情链接更多精彩内容