开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
在Vue中,在父子组件的交互中,我们通常使用的是在子组件上注册一个事件用来触发父组件回调方法这样的方式:
// 父组件
<children @trigger="callback()"></children>
// 子组件
this.$emit("trigger", value)
这个过程是如何实现的呢,在我们之前的_init方法中有这样一个方法:
initEvents(vm)
initEvents方法在src\core\instance\events.ts文件中:
export function initEvents(vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
代码并不复杂,主要就是创建了一个_events对象来存储自己的事件,之后对实例中父组件的事件进行判断,也就是说判断linsteners是否为空,如果不为空,则调用updateComponentListeners方法把事件注册给自己:
export function updateComponentListeners(
vm: Component,
listeners: Object,
oldListeners?: Object | null
) {
target = vm
updateListeners(
listeners,
oldListeners || {},
add,
remove,
createOnceHandler,
vm
)
target = undefined
}
let target: any
function add(event, fn) {
target.$on(event, fn)
}
function remove(event, fn) {
target.$off(event, fn)
}
可以看到,这里依然没有最终实现事件监听,而只是将实例和添加、删除监听的方法传入到updateListeners中进行进一步操作,updateListeners方法在src\core\vdom\helpers\update-listeners.ts文件中,最终实现了对事件的监听:
export function updateListeners(
on: Object,
oldOn: Object,
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
let name, cur, old, event
for (name in on) {
cur = on[name]
old = oldOn[name]
event = normalizeEvent(name)
// 这一步对新传入的监听进行了判断,如果传入的值为undefined或null,则在开发模式下抛出错误
// 这也就是我们常见的如果在模板中如果写的方法未定义或未传入方法时控制台中输出的错误
if (isUndef(cur)) {
__DEV__ &&
warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm)
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
add(event.name, cur, event.capture, event.passive, event.params)
} else if (cur !== old) {
old.fns = cur
on[name] = old
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
在这个方法中,对新旧事件监听都分别做了遍历,对比后进行处理,整个的思路就是:
- 新监听中存在而旧监听中不存在的,加入监听
- 旧监听中存在而新监听中不存在的,移除监听
在这个过程中,有一个方法normalizeEvent被用来反向解析事件名称:
const normalizeEvent = cached(
(
name: string
): {
name: string
once: boolean
capture: boolean
passive: boolean
handler?: Function
params?: Array<any>
} => {
const passive = name.charAt(0) === '&'
name = passive ? name.slice(1) : name
const once = name.charAt(0) === '~' // Prefixed last, checked first
name = once ? name.slice(1) : name
const capture = name.charAt(0) === '!'
name = capture ? name.slice(1) : name
return {
name,
once,
capture,
passive
}
}
)
可以看到,&~!分别对应了passive、once、capture修饰符,但是实际使用vue过程中我们并不会使用这样的符号,那么它们是如何生效的呢?实际上在对事件进行解析的时候,做了这样的工作,解析部分的代码在src\compiler\parser\index.ts中:
// 太长了,就截取一部分
export const onRE = /^@|^v-on:/
if (onRE.test(name)) {
// v-on
name = name.replace(onRE, '')
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
}
而addHandler方法中则进行了这样的处理:
export function addHandler(
el: ASTElement,
name: string,
value: string,
modifiers?: ASTModifiers | null,
important?: boolean,
warn?: Function,
range?: Range,
dynamic?: boolean
) {
……
// check capture modifier
if (modifiers.capture) {
delete modifiers.capture
name = prependModifierMarker('!', name, dynamic)
}
if (modifiers.once) {
delete modifiers.once
name = prependModifierMarker('~', name, dynamic)
}
/* istanbul ignore if */
if (modifiers.passive) {
delete modifiers.passive
name = prependModifierMarker('&', name, dynamic)
}
……
}
总的来说,initEvents方法实际上初始化的就是父组件模板中使用@或者v-on注册的事件,将其遍历进行对比后,存储到子组件的_events对象中,以便实现后续的监听处理。