Vue 里的事件主要有两种,第一种是绑定再原生 DOM 上的事件,第二种是绑定在组件上的自定义事件。文章会详细对两者的相同点和不同点展开讲解。
基本使用
在 template 中使用 v-on
或者其语法糖 `` 可以快速的在节点上添加事件。
同时可以添加修饰符来对事件触发方式和其副作用进行快速的设定。
<div id="app">
<button v-on:click="console.log('DOM Listener')">button</button>
<EventComponentExample @click="ListenerComponentEvent" />
</div>
原理
AST
我们先查看 template 对应生成 AST 是怎么样的。
由于 AST 阶段无法判断节点是原生 DOM 还是组件,所以在这个阶段节点事件的编译是没有区分的。
[
{
"type": 1,
"tag": "button",
"attrsList": [
{
"name": "v-on:click",
"value": "console.log('DOM Listener')"
}
],
"attrsMap": {
"v-on:click": "console.log('DOM Listener')"
},
"children": [
{
"type": 3,
"text": "button",
"static": true
}
],
"hasBindings": true,
"events": {
"click": {
"value": "console.log('DOM Listener')",
"dynamic": false
}
}
},
{
"type": 1,
"tag": "EventComponentExample",
"attrsList": [
{
"name": "@click",
"value": "ListenerComponentEvent"
}
],
"attrsMap": {
"@click": "ListenerComponentEvent"
},
"hasBindings": true,
"events": {
"click": {
"value": "ListenerComponentEvent",
"dynamic": false
}
}
}
]
我们可以发现,对于其他普通的节点来说,主要有两个 AST 属性发生了变化,分别是 hasBinding(是否是有数值或者事件绑定), events(具体绑定的事件的信息,以 key-value 的形式存储着),我们来研究一下其编译过程。
- 遇到节点头标签(
- 遇到节点闭合标签()
- parseEndTag:节点序列化
- option.end:操作节点堆栈
- closeElement:闭合节点操作
- processElement:分析节点内容
- …解析 key/ref/slots/component 等信息
- processAttrs:分析节点属性
- processElement:分析节点内容
- closeElement:闭合节点操作
// 只有 Vue 的指令或者属性才能进入
if (dirRE.test(name)) {
// 确定绑定了内容
el.hasBindings = true;
// 匹配可能存在的修饰符
modifiers = parseModifiers(name.replace(dirRE, ""));
// 还原真实 key 值
name = name.replace(modifierRE, "");
// 是否是事件绑定的指令 v-on/@
if (onRE.test(name)) { // v-on
// 除去指令前缀
name = name.replace(onRE, '')
// 判断是否为动态指令
isDynamic = dynamicArgRE.test(name)
// 如果是动态,去除两边的方括号,得到真实 name
if (isDynamic) {
name = name.slice(1, -1)
}
// 在节点 events 上添加事件信息
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
}
}
CodeGen
AST 后面紧接着就是生成 render 函数,直接看结果
with (this) {
return _c(
"div",
{ attrs: { id: "app" } },
[
_c(
"button",
{
on: {
click: function ($event) {
return console.log("DOM Listener");
},
},
},
[_v("button")]
),
_c("EventComponentExample", { on: { click: ListenerComponentEvent } }),
],
1
);
}
很明显,这里原生 DOM 事件和组件事件还没有区分,render 函数与其他 render 函数的区别也只体现在属性上,多了一个 on 属性,但是 内联处理器 却生成了一个生成的函数,让我们看看源码,Vue 是如何做到的。
genElement:生成节点代码
- genData:生成属性
- genHandlers:遍历 AST 中的 events 属性生成具体代码
- genHandler:生成具体绑定的代码
```javascript
function genHandler (handler: ASTElementHandler | Array
): string { // 如果入参为空,直接返回空函数 if (!handler) {return ‘function(){}’}
- genHandler:生成具体绑定的代码
```javascript
function genHandler (handler: ASTElementHandler | Array
- genHandlers:遍历 AST 中的 events 属性生成具体代码
// 如果是数组则递归生成对应事件集合 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 { // 如果有修饰符,则根据具体修饰返回具体的变形代码 } } ```
- genData:生成属性
VNode
在生成 VNode 的时候,原生 DOM 事件和自定义组件事件变换发生区别,原生 DOM 的事件依旧在 VNode.data.on
上面,而自定义组件的事件,则会转移到 VNode.componentOptions.listeners
上。
我们可以看一下自定义组件的事件是怎么转移的
// _createElement
function _createElement(context, tag, data, children, normalizationType){
//...
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 创建组件的 VNode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
//...
}
// createComponent
export function createComponent (Ctor, data, context, children, tag): VNode | Array<VNode> | void {
//...
const listeners = data.on
data.on = data.nativeOn
//...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
}
原生 DOM 事件绑定过程
createEle
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// 防止引用污染问题,导致判断出错,克隆一边 vnode 进行操作
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
// 如果是组件则会在 createComponent 创建成功,并结束
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// 非组件的节点会走到此处
// 节点属性
const data = vnode.data
// 节点的子节点
const children = vnode.children
// tag 名
const tag = vnode.tag
if (isDef(tag)) {
// 创建真实 DOM,有命名空间的节点会特殊处理
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// 设置 style scope
setScope(vnode)
// 创建子节点
createChildren(vnode, children, insertedVnodeQueue)
// 如果节点有属性,则调用一系列创建函数,来更新节点属性
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 插入父节点
insert(parentElm, vnode.elm, refElm)
} else if (isTrue(vnode.isComment)) {
// 创建注释节点并插入
} else {
// 创建文本节点并插入
}
}
createComponent & initComponent
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// 是否需是重新激活的
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 调用 init 函数创建组件实例
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// 如果创建成功
if (isDef(vnode.componentInstance)) {
// 初始化节点的属性!!!
initComponent(vnode, insertedVnodeQueue)
// 插入真实 DOM
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
// 重新激活
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
// 这里与真实 DOM 创建的收尾流程一样,所以关键就在 invokeCreataHooks 这个函数里面
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
invokeCreateHooks & updateDOMListeners
function invokeCreateHooks (vnode, insertedVnodeQueue) {
// 调用创建相关函数,其中有 "updateAttrs", "updateClass", "updateDOMListeners", "updateDOMProps", "updateStyle, ....
// 其中跟事件有关的核心函数就 updateDOMListeners
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
// 如果没有新旧 VNode 都没有绑定事件则跳过
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
// 真实 DOM
target = vnode.elm
// 格式化事件
normalizeEvents(on)
// 通过 add, remove 更新事件
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
target = undefined
}
add
function add (
name: string,
handler: Function,
capture: boolean,
passive: boolean
) {
// 边缘情况处理
if (useMicrotaskFix) {
const attachedTimestamp = currentFlushTimestamp
const original = handler
handler = original._wrapper = function (e) {
if (
e.target === e.currentTarget ||
e.timeStamp >= attachedTimestamp ||
e.timeStamp <= 0 ||
e.target.ownerDocument !== document
) {
return original.apply(this, arguments)
}
}
}
// 在节点上添加事件
target.addEventListener(
name,
handler,
supportsPassive
? { capture, passive }
: capture
)
}
自定义组件事件绑定及触发过程
前文已经提到,所有绑定在组件上的事件会绑定到 VNode.componentOptions.listeners
上。
在初始化中,在合并选项的时候,在 initInternalComponent
里将值赋值到 options._parentsListeners
上
并在 initEvent
中调用 updateComponentListeners
使用先前在原生 DOM 事件绑定中的提到的 updateListeners
,只不过有区别的是 add
函数不再是 addEventListener
而是 $on
,remove
是 $off
。initRender
的时候 this.$listeners
的代理会指向 options._parentsListeners
$on
$on 负责将监听事件们 push 到 _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
$emit 则是负责从 _events
中取出对应的事件,并调用触发
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
}
总结
原生 DOM 事件和自定义组件的事件在生成 虚拟DOM 之前没有什么分别,但再生成虚拟 DOM 后,前者依旧再 VNode.data.on
上,而后者则直接到 VNode.componentOptions.listeners
。前者会通过 addEventListener
添加到 DOM 上,后者会在组件初始化的时候通过中通过 $on
以键值对的形式存放到 _events
中,并可以通过 $emit
触发指定的监听事件。