Vue2.x 源码 - event

上一篇:Vue2.x 源码 - transition-group

Vue 里可以绑定原生的 DOM 事件,也可以绑定自定义事件;这里看看他的源码实现;

编译

1、在 parse 阶段,会执⾏ processAttrs ⽅法,它的定义在 src/compiler/parser/index.js 中:

export const onRE = /^@|^v-on:/
export const dirRE = process.env.VBIND_PROP_SHORTHAND ? /^v-|^@|^:|^\.|^#/ : /^v-|^@|^:|^#/
export const bindRE = /^:|^\.|^v-bind:/
function processAttrs (el) {
    var list = el.attrsList;
    var i, l, name, rawName, value, modifiers, syncGen, isDynamic;
    for (i = 0, l = list.length; i < l; i++) {
      name = rawName = list[i].name; // v-on:click
      value = list[i].value; // doThis
      if (dirRE.test(name)) { // 匹配v-或者@开头的指令
        el.hasBindings = true;
        modifiers = parseModifiers(name.replace(dirRE, ''));// parseModifiers('on:click')
        if (modifiers) {
          name = name.replace(modifierRE, '');
        }
        if (bindRE.test(name)) { // v-bind分支
          // ...留到v-bind指令时分析
        } else if (onRE.test(name)) { // v-on分支
          name = name.replace(onRE, ''); // 拿到真正的事件click
          isDynamic = dynamicArgRE.test(name);// 动态事件绑定
          if (isDynamic) {
            name = name.slice(1, -1);
          }
          addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic);
        } else { // normal directives
         // 其他指令相关逻辑
      } else {}
    }
  }

onRE 匹配 v-on 的正则,bindRE 匹配 v-bind 的正则,dirRE 匹配事件相关的正则;匹配成功后会调用addHandler方法;

src/compiler/helpers.js文件中:

// el是当前解析的AST树
function addHandler (el,name,value,modifiers,important,warn,range,dynamic) {
    modifiers = modifiers || emptyObject;
    // passive 和 prevent不能同时使用,可以参照官方文档说明
    if (
      warn &&
      modifiers.prevent && modifiers.passive
    ) {
      warn(
        'passive and prevent can\'t be used together. ' +
        'Passive handler can\'t prevent default event.',
        range
      );
    }
    // 这部分的逻辑会对特殊的修饰符做字符串拼接的处理,以备后续的使用
    if (modifiers.right) {
      if (dynamic) {
        name = "(" + name + ")==='click'?'contextmenu':(" + name + ")";
      } else if (name === 'click') {
        name = 'contextmenu';
        delete modifiers.right;
      }
    } else if (modifiers.middle) {
      if (dynamic) {
        name = "(" + name + ")==='click'?'mouseup':(" + name + ")";
      } else if (name === 'click') {
        name = 'mouseup';
      }
    }
    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);
    }
    // events 用来记录绑定的事件
    var events;
    if (modifiers.native) {
      delete modifiers.native;
      events = el.nativeEvents || (el.nativeEvents = {});
    } else {
      events = el.events || (el.events = {});
    }
    var newHandler = rangeSetItem({ value: value.trim(), dynamic: dynamic }, range);
    if (modifiers !== emptyObject) {
      newHandler.modifiers = modifiers;
    }
    var handlers = events[name];
    // 绑定的事件可以多个,回调也可以多个,最终会合并到数组中
    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;
  }

1、首先根据 modifier 修饰符对事件名 name 做处理,然后判断修饰符中是否存在native,如果存在则把事件存放在nativeEvents对象中,否则存放在events中;
2、最后判断handlers是否是一个数组,如果是则根据important参数值选择添加到数组头部还是尾部;如果不是一个数组,则继续判断是否已经有值,有值则处理成数组形式;如果既不是数组,也没有值则直接赋值即可。
3、根据这段代码的逻辑,意味着我们可以重复监听同一个事件,并绑定不同的事件名,它们并不会被相互覆盖,而是会在事件被触发的时候,依次调用events数组中的每一个函数;

注意:在生成 ASTElement 的时候会调用 makeAttrsMap 对重复属性进行检测并在开发环境下提示错误信息;

2、当ast生成完毕后,在调用generate时会调用一个非常核心的genData方法,在这个方法中与事件相关的代码如下:

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

genData方法中,无论是 DOM 事件还是自定义事件,都会调用genHandlers方法来处理,其代码如下:

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

1、遍历事件对象 events ,对同⼀个事件名称的事件调⽤ genHandler(name, events[name]) ⽅法;
2、然后根据dynamic属性区分 动态事件名静态事件名,关键点是native事件修饰符,根据修饰符的不同返回不同的拼接结果;

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
    }
    // ...省略代码
    return `function($event){${
      isFunctionInvocation ? `return ${handler.value}` : handler.value
    }}` // inline statement
  } else {
    // 省略修饰符相关代码
  }
}

1、genHandler 方法⾸先先判断如果 handler 是⼀个 数组,是就遍历它然后递归调⽤ genHandler ⽅法并拼接结果;
2、然后对 modifiers (修饰符)做判断,对于没有 modifiers 的情况,就 根据 handler.value 不同情况处理,要么直接返回,要么返回⼀个函数包裹的表达式;
3、对于有 modifiers 的情况,则对各种不同的 modifer 情况做不同处理,添加相应的代码串;

这里主要是对事件的访问方式进行处理:isMethodPath代表是否为简单访问方式、isFunctionExpression代表是否为函数表达式方式、isFunctionInvocation代表是否为函数调用方式;

简单访问方式:

<button @click="handle">Button</button>
<button @click="handle.click">Button</button>
<button @click="handle['click']">Button</button>

函数表达式方式:

<button @click="() => handleClick()">Button</button>

函数调用方式:

<button @click="handleClick()">Button</button>

DOM事件

所有和 web 相关的 module 都定义在 src/platforms/web/runtime/modules ⽬录下,在 patch 过程中的创建阶段和更新阶段都会执⾏ updateDOMListeners,在 src/platforms/web/runtime/modules/events.js 文件中:

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
}

首先获取 vnode.data.on ,这就是我们之前的⽣成的 data 中对应的事件对象, target 是当前 vnode 对于的 DOM 对象, normalizeEvents 主要是对 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)
    /* 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)
    }
  }
}

1、for-in 遍历 on 对象:其作用是用来 add 添加事件监听或者更新事件监听。在此循环中,首先判断了当前事件是否已经定义,如果没有则在开发环境下提示错误信息;如果已经定义,但在旧事件监听中,没有则表示应该使用add来新增这个事件监听;如果当前事件监听和旧事件监听都有,但是不并相同。则表明虽然监听的是同一个事件,但是回调函数不同,此时应该更新事件;
2、for-in 遍历oldOn对象:其作用是用来移除事件监听。在此循环中,判断旧事件监听不在新事件监听中,则表明应该移除这些事件监听,移除事件监听调用了 remove 方法;

normalizeEvent 获得每⼀个事件名,根据我们的的事件名的⼀些特殊标识(之前在 addHandler 的时候添加上的)区分出这个事件是否有 once 、 capture 、 passive 等修饰符;

createFnInvoker 首先定义了一个invoker方法,然后把for-in循环的当前事件监听赋值到invoker方法的fns属性上,随后返回invoker

src/platforms/web/runtime/modules/events.js 文件中:
1、add添加事件监听

function add (
  name: string,
  handler: Function,
  capture: boolean,
  passive: boolean
) {
  // ...省略处理浏览器兼容代码
  target.addEventListener(
    name,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

2、remove移除事件监听

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

addremove 的逻辑很简单,就是实际上调⽤原⽣ addEventListenerremoveEventListener ,并根据参数传递⼀些配置

自定义事件

除了原⽣ DOM 事件,Vue 还⽀持了⾃定义事件,并且⾃定义事件只能作⽤在组件上,如果在组件上使 ⽤原⽣事件,需要加 .native 修饰符,普通元素上使⽤ .native 修饰符⽆效;

render 阶段,如果是⼀个组件节点,则通过 createComponent 创建⼀个组件 vnode ,我们再 来回顾这个⽅法,定义在 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
}

它把 data.on 赋值给了 listeners ,把 data.nativeOn 赋值给了 data.on ,这样所有的原⽣ DOM 事件处理跟我们刚才介绍的⼀样,它是在当前组件环境中处理的;⽽对于⾃定义事件,我们把 listeners 作为 vnodecomponentOptions 传⼊,它是在⼦组件初始化阶段中处理的,所以它的处理环境是⼦组件;

然后在⼦组件的初始化的时候,会执⾏ 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
  // ....省略代码
}

这⾥拿到了⽗组件传⼊的 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) ⽅法:

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

updateComponentListeners方法的代码首先设置事件监听的target为当前子组件实例,然后调用updateListeners方法。这里的updateListeners方法和原生 DOM 事件小节中提到的updateListeners是同一个;区别是,这里传递的 add 方法和remove方法有一点不同;

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

$on$off是 Vue 内置事件系统中的两个方法,这些内置事件方法的定义发生在eventMixin方法中:

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

总结

1、事件的使用方式:普通模式,函数表达式模式、函数调用模式、使用事件修饰符等;
2、Vue ⽀持 2 种事件类型,原⽣ DOM 事件和⾃定义 事件,它们主要的区别在于添加和删除事件的⽅式不⼀样,并且⾃定义事件的派发是往当前实例上派发,但是可以利⽤在⽗组件环境定义回调函数来实现⽗⼦组件的通讯;
3、只有组件节 点才可以添加⾃定义事件,并且添加原⽣ DOM 事件需要使⽤ native 修饰符;⽽普通元素使⽤ .native 修饰符是没有作⽤的,也只能添加原⽣ DOM 事件;

下一篇:Vue2.x 源码 - v-model

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值