到这里为止,Vue 的主要逻辑就已经全部介绍完了。接下来的文章会从其他角度来解析,如事件处理、各个指令的处理等等。先从事件处理开始。
Vue
的事件处理分为两类:DOM
事件和自定义事件,二者走的是完全不同的处理流程,不过在处理子组件时,会联系到一起,后面会说到。
DOM 事件
在模板中通过@
或v-on
指令放在元素节点上的,如:
1 | <div id="app"> |
对于 DOM 事件的处理会依次经过compile
、render
和patch
几个阶段。
compile 阶段转为 AST
在模板编译时,对于节点上的各种属性处理,如静态属性、动态绑定属性、事件绑定等都会放到processAttrs
这个函数中处理,这个函数代码有点多,我只展示关键的逻辑:
1 | function processAttrs(el) { |
addHandler
会在el.nativeEvents
或el.events
对象上添加handler
属性:
1 | export function addHandler(el: ASTElement, name: string, value: string, modifiers: ?ASTModifiers, important?: boolean, warn?: Function) { |
代码还是不难的,主要分为两步: 先处理函数名,将一些内置修饰符转为函数名中的前缀; 之后将处理函数放到events
上。
generate 阶段生成 render 字符串
下一步就到了生成render
了,在genData
函数中会处理el.events
、el.nativeEvents
,放到data.on
、data.nativeOn
上:
1 | export function genData(el: ASTElement, state: CodegenState): string { |
都是调用的同一个genHandlers
:
1 | /** |
针对events
中每种类型的事件处理,调用genHandler
处理events[name]
,events[name]
可能是数组也可能是独立对象,取决于name
是否有多个处理函数。
1 | function genHandler(name: string, handler: ASTElementHandler | Array<ASTElementHandler>): string { |
如果绑定的是函数定义、一个函数路径、没有事件修饰符,处理都很简单。genModifierCode
用于处理携带修饰符的情形,modifierCode
生成内置修饰符的处理:
1 | const modifierCode: { [key: string]: string } = { |
keyCodes
是内置按键别名:
1 | // KeyboardEvent.keyCode aliases |
genKeyFilter
用于生成一段过滤的字符串:
1 | // 返回的是一个判断: 不符合一定条件就return null |
最终我们的函数处理字符串被包裹在function($event){}
函数体当中。
patch 阶段添加事件处理到 DOM 上
在render
函数生成vnode
期间不会处理生成的函数字符串,之后在patch
阶段的invokeCreateHooks
中会调用各个钩子来处理data
对象上的属性(data
对象就是我们上面genData
的返回值),其中就会将data.on
、data.nativeOn
上的事件处理函数添加到DOM
上。在invokeDestroyHook
中又会卸载DOM
上的事件处理函数。
1 | function invokeCreateHooks(vnode, insertedVnodeQueue) { |
处理事件的module
定义在src/platforms/web/runtime/modules/events.js
的updateDOMListeners
:
1 | function updateDOMListeners(oldVnode: VNodeWithData, vnode: VNodeWithData) { |
此处的add
、remove
是两个帮助函数,核心是用addEventListener
、removeEventListener
添加事件处理到DOM
上:
1 | function add(event: string, handler: Function, once: boolean, capture: boolean, passive: boolean) { |
updateListeners
会将data.on
上新增的事件处理利用add
添加到DOM
上,将oldOn
上的过时的事件处理用remove
移除掉:
1 | export function updateListeners(on: Object, oldOn: Object, add: Function, remove: Function, vm: Component) { |
上面的normalizeEvent
就是用来处理在compile
阶段为函数名添加的各种内置修饰符前缀如~ !
等,将他们反向解析会passive
、once
等:
1 | const normalizeEvent = cached( |
以上就是Vue
对于DOM
事件的处理流程了,还是比想象中复杂许多的。
自定义事件
Vue
也可以利用$emit
、$on
、$off
、once
等创建自定义事件,使用方法很简单大家自己去看官网即可。他们的内部实现其实主要也是利用了观察者模式,代码其实挺简单的,挨个看下:
$on
1 | Vue.prototype.$on = function(event: string | Array<string>, fn: Function): Component { |
所有的事件监听都会放到vm
内部的_events
上,按照事件名进行分类。
$once
1 | Vue.prototype.$once = function(event: string, fn: Function): Component { |
$once
和$on
很像,唯一的差别是$once
在执行完一次后就会利用$off
卸载掉。
$off
1 | Vue.prototype.$off = function(event?: string | Array<string>, fn?: Function): Component { |
用于卸载一个事件监听,代码虽然看起来挺长,其实就是考虑了多种参数情况而已,可能是卸载全部所有事件处理、卸载指定名称的全部事件处理、卸载指定名称的指定事件处理。
$emit
用于触发事件:
1 | Vue.prototype.$emit = function(event: string): Component { |
以上就是自定义事件的全部处理,很简单。
自定义组件 DOM 事件
相信大家也看出来了,上面说的DOM
事件和自定义事件完全没有任何关联。但在处理自定义组件时,貌似又说不通,例如:
1 | <div id="app"><my-component @click="change"></my-component></div> |
我们在my-component
上添加了一个DOM
事件,并期望点击时可以出现log
日志,但实际上change
事件并没有被调用。
如果我们修改下template
:
1 | <my-component @click.native="change"></my-component> |
很神奇此时log
可以出现。
或者只改写子组件自身的逻辑:
1 | myComponent: { |
不出所料,此时log
也是可以出现的。
很明显,在处理自定义组件时,Vue
会做一些处理以将二者联系起来。回顾下我们此前介绍的Vue
生命周期,最开始处理子组件的地方就是render
生成vnode
了。在src/core/vdom/create-component.js
的createComponent
方法就是用于生成自定义组件的vnode
,其中有一段:
1 | export function createComponent( |
看到上面关于事件处理的两句赋值:
1 | const listeners = data.on; |
我们.native
事件处理放到data.on
上,再将其他事件监听放到listeners
上,那么再什么时候会处理这两个属性呢?没错,还是在patch
阶段。
patch
会在不同时候调用在vnode.data
上不同的钩子,这些钩子是在installComponentHooks
方法中被赋值的,主要有 4 个钩子:init
、prepatch
、insert
、destroy
,他们定义在src/core/vdom/create-component.js
的componentVNodeHooks
对象上,只有自定义子组件会拥有这些钩子。
1 | const componentVNodeHooks = { |
接下来我们看看这些钩子具体在何时调用。
patch
在初始化的某个时候会调用createComponent
方法,在这里会调用init
钩子:
1 | function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) { |
init
钩子主要会将子组件挂载到vnode.elm
上:
1 | init (vnode: VNodeWithData, hydrating: boolean): ?boolean { |
$mount
操作就又回到了我们很早前将的Vue
生命周期了,只不过针对子组件会特殊处理listeners
属性,回顾上我们刚刚说的在子组件render
时会将data.on
赋值给listeners
。处理listeners
是在src/core/instance/events.js
的initEvents
方法:
1 | export function initEvents(vm: Component) { |
updateComponentListeners
底层依然调用的是updateListeners
,这与我们此前说DOM
事件处理时调用的是同一个函数,不同的是add
和remove
参数:
1 | function add(event, fn, once) { |
target
是子组件的vm
,listeners
是父组件template
中定义在子组件标签上的事件处理函数。我们的add
和remove
在这里其实利用的是$on/$once
和$off
,而不是addEventListeners
和removeEventListeners
。
所以真相大白了:父组件针对子组件的非native
事件监听函数,其实最后是传到了子组件内部的自定义事件处理流程中,只不过这些函数已经绑定了父组件的this
。
那么native
事件呢?
在patch
中调用完子组件的init
钩子后,会继续调用initComponent
函数:
1 | function initComponent(vnode, insertedVnodeQueue) { |
我们的native
事件在生成vnode
时被放到了data.on
上,在invokeCreateHooks
中就会被处理:events
这个子module
会将data.on
上的事件处理函数放到vnode.elm
也就是子组件的根元素上,底层用的是addEventListener/removeEventListener
,具体请看此前讲解patch
处理的文章。
这样我们就知道了:父组件放到子组件标签上的native
事件处理,其实最后还是放到了子组件的根元素上,这样当子组件中的元素节点触发事件时,就会冒泡到子组件根元素上,事件得到处理。事件处理函数的this
依然事先就被绑定到了父组件的vm
上。