Vue 里的事件主要有两种,第一种是绑定再原生 DOM 上的事件,第二种是绑定在组件上的自定义事件。文章会详细对两者的相同点和不同点展开讲解。

基本使用

在 template 中使用 v-on 或者其语法糖 `` 可以快速的在节点上添加事件。
同时可以添加修饰符来对事件触发方式和其副作用进行快速的设定。

  1. <div id="app">
  2. <button v-on:click="console.log('DOM Listener')">button</button>
  3. <EventComponentExample @click="ListenerComponentEvent" />
  4. </div>

原理

AST

我们先查看 template 对应生成 AST 是怎么样的。
由于 AST 阶段无法判断节点是原生 DOM 还是组件,所以在这个阶段节点事件的编译是没有区分的。

  1. [
  2. {
  3. "type": 1,
  4. "tag": "button",
  5. "attrsList": [
  6. {
  7. "name": "v-on:click",
  8. "value": "console.log('DOM Listener')"
  9. }
  10. ],
  11. "attrsMap": {
  12. "v-on:click": "console.log('DOM Listener')"
  13. },
  14. "children": [
  15. {
  16. "type": 3,
  17. "text": "button",
  18. "static": true
  19. }
  20. ],
  21. "hasBindings": true,
  22. "events": {
  23. "click": {
  24. "value": "console.log('DOM Listener')",
  25. "dynamic": false
  26. }
  27. }
  28. },
  29. {
  30. "type": 1,
  31. "tag": "EventComponentExample",
  32. "attrsList": [
  33. {
  34. "name": "@click",
  35. "value": "ListenerComponentEvent"
  36. }
  37. ],
  38. "attrsMap": {
  39. "@click": "ListenerComponentEvent"
  40. },
  41. "hasBindings": true,
  42. "events": {
  43. "click": {
  44. "value": "ListenerComponentEvent",
  45. "dynamic": false
  46. }
  47. }
  48. }
  49. ]

我们可以发现,对于其他普通的节点来说,主要有两个 AST 属性发生了变化,分别是 hasBinding(是否是有数值或者事件绑定), events(具体绑定的事件的信息,以 key-value 的形式存储着),我们来研究一下其编译过程。

  1. // 只有 Vue 的指令或者属性才能进入
  2. if (dirRE.test(name)) {
  3. // 确定绑定了内容
  4. el.hasBindings = true;
  5. // 匹配可能存在的修饰符
  6. modifiers = parseModifiers(name.replace(dirRE, ""));
  7. // 还原真实 key 值
  8. name = name.replace(modifierRE, "");
  9. // 是否是事件绑定的指令 v-on/@
  10. if (onRE.test(name)) { // v-on
  11. // 除去指令前缀
  12. name = name.replace(onRE, '')
  13. // 判断是否为动态指令
  14. isDynamic = dynamicArgRE.test(name)
  15. // 如果是动态,去除两边的方括号,得到真实 name
  16. if (isDynamic) {
  17. name = name.slice(1, -1)
  18. }
  19. // 在节点 events 上添加事件信息
  20. addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
  21. }
  22. }

CodeGen

AST 后面紧接着就是生成 render 函数,直接看结果

  1. with (this) {
  2. return _c(
  3. "div",
  4. { attrs: { id: "app" } },
  5. [
  6. _c(
  7. "button",
  8. {
  9. on: {
  10. click: function ($event) {
  11. return console.log("DOM Listener");
  12. },
  13. },
  14. },
  15. [_v("button")]
  16. ),
  17. _c("EventComponentExample", { on: { click: ListenerComponentEvent } }),
  18. ],
  19. 1
  20. );
  21. }

很明显,这里原生 DOM 事件和组件事件还没有区分,render 函数与其他 render 函数的区别也只体现在属性上,多了一个 on 属性,但是 内联处理器 却生成了一个生成的函数,让我们看看源码,Vue 是如何做到的。

  • genElement:生成节点代码

    • genData:生成属性
      • genHandlers:遍历 AST 中的 events 属性生成具体代码
        • genHandler:生成具体绑定的代码 ```javascript function genHandler (handler: ASTElementHandler | Array): 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) {

    1. return handler.value

    } // 如果是行内立即调用则封装一个函数直接返回,如果是内联处理器,则在新的而函数内直接执行 return `function($event){${

    1. isFunctionInvocation ? `return ${handler.value}` : handler.value

    }}` // inline statement } else { // 如果有修饰符,则根据具体修饰返回具体的变形代码 } } ```

VNode

在生成 VNode 的时候,原生 DOM 事件和自定义组件事件变换发生区别,原生 DOM 的事件依旧在 VNode.data.on 上面,而自定义组件的事件,则会转移到 VNode.componentOptions.listeners 上。
我们可以看一下自定义组件的事件是怎么转移的

  1. // _createElement
  2. function _createElement(context, tag, data, children, normalizationType){
  3. //...
  4. } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
  5. // 创建组件的 VNode
  6. vnode = createComponent(Ctor, data, context, children, tag)
  7. } else {
  8. //...
  9. }
  10. // createComponent
  11. export function createComponent (Ctor, data, context, children, tag): VNode | Array<VNode> | void {
  12. //...
  13. const listeners = data.on
  14. data.on = data.nativeOn
  15. //...
  16. const vnode = new VNode(
  17. `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
  18. data, undefined, undefined, undefined, context,
  19. { Ctor, propsData, listeners, tag, children },
  20. asyncFactory
  21. )
  22. }

原生 DOM 事件绑定过程

createEle

  1. function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
  2. if (isDef(vnode.elm) && isDef(ownerArray)) {
  3. // 防止引用污染问题,导致判断出错,克隆一边 vnode 进行操作
  4. vnode = ownerArray[index] = cloneVNode(vnode)
  5. }
  6. vnode.isRootInsert = !nested // for transition enter check
  7. // 如果是组件则会在 createComponent 创建成功,并结束
  8. if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  9. return
  10. }
  11. // 非组件的节点会走到此处
  12. // 节点属性
  13. const data = vnode.data
  14. // 节点的子节点
  15. const children = vnode.children
  16. // tag 名
  17. const tag = vnode.tag
  18. if (isDef(tag)) {
  19. // 创建真实 DOM,有命名空间的节点会特殊处理
  20. vnode.elm = vnode.ns
  21. ? nodeOps.createElementNS(vnode.ns, tag)
  22. : nodeOps.createElement(tag, vnode)
  23. // 设置 style scope
  24. setScope(vnode)
  25. // 创建子节点
  26. createChildren(vnode, children, insertedVnodeQueue)
  27. // 如果节点有属性,则调用一系列创建函数,来更新节点属性
  28. if (isDef(data)) {
  29. invokeCreateHooks(vnode, insertedVnodeQueue)
  30. }
  31. // 插入父节点
  32. insert(parentElm, vnode.elm, refElm)
  33. } else if (isTrue(vnode.isComment)) {
  34. // 创建注释节点并插入
  35. } else {
  36. // 创建文本节点并插入
  37. }
  38. }

createComponent & initComponent

  1. function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  2. let i = vnode.data
  3. if (isDef(i)) {
  4. // 是否需是重新激活的
  5. const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
  6. // 调用 init 函数创建组件实例
  7. if (isDef(i = i.hook) && isDef(i = i.init)) {
  8. i(vnode, false /* hydrating */)
  9. }
  10. // 如果创建成功
  11. if (isDef(vnode.componentInstance)) {
  12. // 初始化节点的属性!!!
  13. initComponent(vnode, insertedVnodeQueue)
  14. // 插入真实 DOM
  15. insert(parentElm, vnode.elm, refElm)
  16. if (isTrue(isReactivated)) {
  17. // 重新激活
  18. reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
  19. }
  20. return true
  21. }
  22. }
  23. }
  24. function initComponent (vnode, insertedVnodeQueue) {
  25. if (isDef(vnode.data.pendingInsert)) {
  26. insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
  27. vnode.data.pendingInsert = null
  28. }
  29. vnode.elm = vnode.componentInstance.$el
  30. if (isPatchable(vnode)) {
  31. // 这里与真实 DOM 创建的收尾流程一样,所以关键就在 invokeCreataHooks 这个函数里面
  32. invokeCreateHooks(vnode, insertedVnodeQueue)
  33. setScope(vnode)
  34. } else {
  35. // empty component root.
  36. // skip all element-related modules except for ref (#3455)
  37. registerRef(vnode)
  38. // make sure to invoke the insert hook
  39. insertedVnodeQueue.push(vnode)
  40. }
  41. }

invokeCreateHooks & updateDOMListeners

  1. function invokeCreateHooks (vnode, insertedVnodeQueue) {
  2. // 调用创建相关函数,其中有 "updateAttrs", "updateClass", "updateDOMListeners", "updateDOMProps", "updateStyle, ....
  3. // 其中跟事件有关的核心函数就 updateDOMListeners
  4. for (let i = 0; i < cbs.create.length; ++i) {
  5. cbs.create[i](emptyNode, vnode)
  6. }
  7. i = vnode.data.hook // Reuse variable
  8. if (isDef(i)) {
  9. if (isDef(i.create)) i.create(emptyNode, vnode)
  10. if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  11. }
  12. }
  13. function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  14. // 如果没有新旧 VNode 都没有绑定事件则跳过
  15. if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
  16. return
  17. }
  18. const on = vnode.data.on || {}
  19. const oldOn = oldVnode.data.on || {}
  20. // 真实 DOM
  21. target = vnode.elm
  22. // 格式化事件
  23. normalizeEvents(on)
  24. // 通过 add, remove 更新事件
  25. updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
  26. target = undefined
  27. }

add

  1. function add (
  2. name: string,
  3. handler: Function,
  4. capture: boolean,
  5. passive: boolean
  6. ) {
  7. // 边缘情况处理
  8. if (useMicrotaskFix) {
  9. const attachedTimestamp = currentFlushTimestamp
  10. const original = handler
  11. handler = original._wrapper = function (e) {
  12. if (
  13. e.target === e.currentTarget ||
  14. e.timeStamp >= attachedTimestamp ||
  15. e.timeStamp <= 0 ||
  16. e.target.ownerDocument !== document
  17. ) {
  18. return original.apply(this, arguments)
  19. }
  20. }
  21. }
  22. // 在节点上添加事件
  23. target.addEventListener(
  24. name,
  25. handler,
  26. supportsPassive
  27. ? { capture, passive }
  28. : capture
  29. )
  30. }

自定义组件事件绑定及触发过程

前文已经提到,所有绑定在组件上的事件会绑定到 VNode.componentOptions.listeners 上。
在初始化中,在合并选项的时候,在 initInternalComponent 里将值赋值到 options._parentsListeners
并在 initEvent 中调用 updateComponentListeners 使用先前在原生 DOM 事件绑定中的提到的 updateListeners ,只不过有区别的是 add 函数不再是 addEventListener 而是 $onremove$off
initRender 的时候 this.$listeners 的代理会指向 options._parentsListeners

所以事件绑定和触发的过程主要是 $on 和 $event

$on

$on 负责将监听事件们 push 到 _events

  1. Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  2. const vm: Component = this
  3. if (Array.isArray(event)) {
  4. for (let i = 0, l = event.length; i < l; i++) {
  5. vm.$on(event[i], fn)
  6. }
  7. } else {
  8. (vm._events[event] || (vm._events[event] = [])).push(fn)
  9. // optimize hook:event cost by using a boolean flag marked at registration
  10. // instead of a hash lookup
  11. if (hookRE.test(event)) {
  12. vm._hasHookEvent = true
  13. }
  14. }
  15. return vm
  16. }

$emit

$emit 则是负责从 _events 中取出对应的事件,并调用触发

  1. Vue.prototype.$emit = function (event: string): Component {
  2. const vm: Component = this
  3. let cbs = vm._events[event]
  4. if (cbs) {
  5. cbs = cbs.length > 1 ? toArray(cbs) : cbs
  6. const args = toArray(arguments, 1)
  7. const info = `event handler for "${event}"`
  8. for (let i = 0, l = cbs.length; i < l; i++) {
  9. invokeWithErrorHandling(cbs[i], vm, args, vm, info)
  10. }
  11. }
  12. return vm
  13. }

总结

原生 DOM 事件和自定义组件的事件在生成 虚拟DOM 之前没有什么分别,但再生成虚拟 DOM 后,前者依旧再 VNode.data.on 上,而后者则直接到 VNode.componentOptions.listeners。前者会通过 addEventListener 添加到 DOM 上,后者会在组件初始化的时候通过中通过 $on 以键值对的形式存放到 _events 中,并可以通过 $emit 触发指定的监听事件。