Vue的事件绑定主要是通过v-on
指令来实现的,这个指令既可以实现原生事件绑定,例如onclick
等。也可以实现组件的自定义事件,从而实现组件的数据通信。
本文我们就来分析下Vue的事件处理的逻辑。
v-on
作用于普通元素
用在普通元素上时,只能监听原生 DOM 事件,最多的就是onclick
事件了。我们就以onclick
事件来分析原理。
案例
let click = () => {
console.log("点击我,很快乐")
};
<!-- template -->
<div v-on:click="click">点击我吧</div>
分析实现逻辑
- 我们先来看下渲染函数
const _hoisted_1 = ["onClick"]
function render(_ctx, _cache) {
with (_ctx) {
const { openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", { onClick: click }, "点击我吧", 8 /* PROPS */, _hoisted_1))
}
}
我们看到 渲染函数在创建VNode的时候传了一个
onClick
的pros
;
- 我们先来看下
patchProp
函数中对onClick
这个pros
的处理逻辑
export const patchProp: DOMRendererOptions['patchProp'] = (
el,
key,
prevValue,
nextValue,
isSVG = false,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
) => {
if (isOn(key)) {
patchEvent(el, key, prevValue, nextValue, parentComponent)
}
}
function patchEvent(el, rawName, prevValue, nextValue, instance = null) {
// vei = vue event invokers
const invokers = el._vei || (el._vei = {});
if (添加) {
const invoker = (invokers[rawName] = createInvoker(nextValue, instance));
el.addEventListener(event, handler, options)
} else {
el.removeEventListener(event, handler, options)
}
}
我们可以看出来底层就是调用的
addEventListener
函数进行事件监听绑定,调用removeEventListener
进行事件监听解绑。其实这个实现逻辑很容易想到,没什么难度。
重点分析—事件修饰符
-
once
,capture
,passive
这两个可以直接作为
addEventListener
和removeEventListener
的第三个参数options
中的值,因为这是W3C支持的事件可选参数。
-
stop
,prevent
,capture
,self
等。
这类修饰符被封装在另外一个
withModifiers
函数中。
export const withModifiers = (fn: Function, modifiers: string[]) => {
return (event: Event, ...args: unknown[]) => {
for (let i = 0; i < modifiers.length; i++) {
const guard = modifierGuards[modifiers[i]]
if (guard && guard(event, modifiers)) return
}
return fn(event, ...args)
}
}
这里设计的非常精妙,每个修饰符都对应一个执行函数,如果调用执行函数
guard(event, modifiers)
返回true
, 则函数withModifiers
就直接返回了,不会再执行事件的函数fn(event, ...args)
了。
这里列一些这些修饰符对应的函数:
const modifierGuards: Record<
string,
(e: Event, modifiers: string[]) => void | boolean
> = {
stop: e => e.stopPropagation(),
prevent: e => e.preventDefault(),
self: e => e.target !== e.currentTarget,
ctrl: e => !(e as KeyedEvent).ctrlKey,
shift: e => !(e as KeyedEvent).shiftKey,
alt: e => !(e as KeyedEvent).altKey,
meta: e => !(e as KeyedEvent).metaKey,
left: e => 'button' in e && (e as MouseEvent).button !== 0,
middle: e => 'button' in e && (e as MouseEvent).button !== 1,
right: e => 'button' in e && (e as MouseEvent).button !== 2,
exact: (e, modifiers) =>
systemModifiers.some(m => (e as any)[`${m}Key`] && !modifiers.includes(m))
}
v-on
作用于组件绑定自定义事件
实现案例
- 父组件中 有个子组件
son
, 使用v-on
绑定了子组件的自定义事件,还有一个p
显示当前的时间戳。
<Son v-on:children-clicked="childClickedHandler" />
<p>{{ date }}</p>
setup() {
let childClickedHandler = (data: Date) => {
date.value = data.getTime();
}
let date = ref(new Date().getTime());
return {
date,
childClickedHandler
};
},
- 子组件中有一个
div
, 每次点击会触发自定义事件childrenClicked
, 并且传递了一个参数值为当前时间。
<div v-on:click="clickevent">点击我吧</div>
emits: ["childrenClicked"],
setup(props, {emit}) {
let clickevent = () => {
emit('childrenClicked', new Date());
}
return {clickevent};
},
这样点击子组件后就会触发父组件的
childClickedHandler
方法,从而更新当前时间戳的显示。
接下来我们就来看看这底层的逻辑是如何实现的?
实现逻辑
- 先看下两个组件的渲染函数的重点部分
父组件:
_createVNode(_component_Son, { onChildrenClicked: childClickedHandler }, null, 8 /* PROPS */, ["onChildrenClicked"])
父组件给子组件绑定自定义事件是传递了一个事件
pro
,这个pro
的名称用驼峰命名, 例如本例中的onChildrenClicked
。
子组件:
const _hoisted_1 = ["onClick"]
_createElementBlock("div", {
onClick: $event => ($emit('childrenClicked', new Date()))
}, "点击我吧", 8 /* PROPS */, _hoisted_1)
子组件
div
点击的绑定前面说过,点击的时候执行$emit('childrenClicked', new Date()
, 这个没有什么特别的。
现在的问题就是为什么子组件$emit('childrenClicked', new Date()
如何找到父组件的onChildrenClicked
方法并执行?
-
$emit
来自于createSetupContext
函数调用时候传入的参数setupContext
export function createSetupContext(
instance: ComponentInternalInstance
): SetupContext {
return {
get attrs() {
return attrs || (attrs = createAttrsProxy(instance))
},
slots: instance.slots,
emit: instance.emit,
expose
}
}
}
$emit
就是组件实例的emit
方法。
- 实例的
emit
方法用于寻找对应的自定义事件的函数
export function emit(
instance: ComponentInternalInstance,
event: string,
...rawArgs: any[]
) {
const props = instance.vnode.props || EMPTY_OBJ
// 传入的传参
let args = rawArgs
// TODO: 处理v-mode的方法
const isModelListener = event.startsWith('update:')
// 处理函数名,on+首字母大写的函数名 或者 on+驼峰命名的函数名
let handlerName
let handler =
props[(handlerName = toHandlerKey(event))] ||
props[(handlerName = toHandlerKey(camelize(event)))]
if (!handler && isModelListener) {
handler = props[(handlerName = toHandlerKey(hyphenate(event)))]
}
if (handler) {
// 调用函数,参数是外部传入的参数
callWithAsyncErrorHandling(
handler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
}
- 如果函数名以
update:
开头,说明是一个v-model
的修改数据函数,这部分逻辑会在v-model
专门的文章中介绍;- 然后在实例对象的
props
中找on+首字母大写的函数名
的函数,如果没找到,则找on+首字母大写且驼峰命名的函数名
的函数;- 如果找到了对应的函数,则调用函数,调用函数的参数为传入的参数。
总结
-
v-on
作用于普通元素底层是利用addEventListener
和removeEventListener
,修饰符要么利用W3C标准,要么利用函数调用来实现; -
v-on
作用于组件是 子组件利用emit
在pro
中搜寻到对应的函数(由父组件传入),然后执行对应的函数。