到这里为止,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上。