本文已参与「新人创作礼」活动,一起开启掘金创作之路。
之前我们已经介绍了event
的编译过程(点击这里跳转),接下来我们分析在Vue
初始化和更新的过程中event
是如何生成的。
event生成之原生DOM事件
Vue
中event
事件分为原生DOM事件与自定义事件,在普通元素节点上只能绑定原生DOM
事件。Vue
内部的DOM
事件在初次更新的时候如何生成,在更新元素节点时DOM
事件如何更新,下面我们来通过源码看一下这个过程:
1.初始化
无论是在DOM节点初始化还是更新的时候都会执行updateDOMListeners
函数来初始化事件和更新事件,源码如下:
updateDOMListeners
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
// 储存新节点的on属性
const on = vnode.data.on || {}
// 储存旧节点的on属性,初始化的时候为空
const oldOn = oldVnode.data.on || {}
// 当前节点的elm
target = vnode.elm
// 处理v-model,这里跳过
normalizeEvents(on)
// 执行updateListeners
updateListeners(on, oldOn, add, remove, vnode.context)
target = undefined
}
初始化的时候,oldVnode
未定义,这里会做下缓存,再将当前的真实节点储存在target
中,执行updateListeners
。
updateListeners:
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
vm: Component
) {
let name, def, cur, old, event
// 遍历on对象
for (name in on) {
// 拿到新值
def = cur = on[name]
// 拿到旧值,初始化的时候为空
old = oldOn[name]
// 解析修饰符,返回解析对象
event = normalizeEvent(name)
......
if (isUndef(cur)) {
// cur未定义,报错
process.env.NODE_ENV !== 'production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) {
// 初始化的时候old不存在,进入该逻辑
if (isUndef(cur.fns)) {
// 初始化的时候cur上未定义fns,进入该逻辑,调用createFnInvoker,cur为参数
cur = on[name] = createFnInvoker(cur)
}
// 调用add方法,将event对象属性当做参数传入
add(event.name, cur, event.once, event.capture, event.passive, event.params)
} else if (cur !== old) {
// 更新的时候进入该逻辑
old.fns = cur
on[name] = old
}
}
// 遍历oldOn, 初始化的时候没有oldOn
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
// 之前编译的过程中,将相应的修饰符转换成了'&'、'~'、'!',这里转换出来,处理成对象返回
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
}
})
这里会遍历编译生成的on
对象中,拿到其中的事件,解析其中的修饰符,返回事件对象,调用createFnInvoker
函数生成最终的执行函数:
createFnInvoker
export function createFnInvoker (fns: Function | Array<Function>): Function {
// 定义invoker函数
function invoker () {
// 拿到fns,也就是传入的函数
const fns = invoker.fns
// 如果是函数数组,遍历数组,依次执行里面的函数
if (Array.isArray(fns)) {
const cloned = fns.slice()
for (let i = 0; i < cloned.length; i++) {
cloned[i].apply(null, arguments)
}
} else {
// 定义的普通函数,直接调用
// return handler return value for single handlers
return fns.apply(null, arguments)
}
}
// 传入的函数赋值给fns
invoker.fns = fns
// 返回invoker函数
return invoker
}
再将生成的函数及解析后的事件对象当做参数传入add
函数,调用add
函数:
add
function add (
event: string,
handler: Function,
once: boolean,
capture: boolean,
passive: boolean
) {
// 包装一层,异步执行的时候,进入宏任务
handler = withMacroTask(handler)
// 如果定义了once,将函数做一层包装,执行一次后销毁该函数
if (once) handler = createOnceHandler(handler, event, capture)
// 这儿绑定原生事件
target.addEventListener(
event, // 事件名
handler, // 函数
supportsPassive
? { capture, passive }
: capture
)
}
/**
* Wrap a function so that if any code inside triggers state change,
* the changes are queued using a (macro) task instead of a microtask.
*/
// 包装一层,使用宏任务执行函数
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
// 给函数包装一下,函数只能执行一次
function createOnceHandler (handler, event, capture) {
const _target = target // save current target element in closure
return function onceHandler () {
const res = handler.apply(null, arguments)
if (res !== null) {
remove(event, onceHandler, capture, _target)
}
}
}
add函数的作用就是给实际的DOM
节点添加原生事件。
2.更新
更新的过程也同样会执行updateDOMListeners
函数:
updateDOMListeners
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
vm: Component
) {
let name, def, cur, old, event
// 遍历on对象
for (name in on) {
// 拿到新值
def = cur = on[name]
// 拿到旧值,初始化的时候为空
old = oldOn[name]
// 解析修饰符,返回解析对象
event = normalizeEvent(name)
......
if (isUndef(cur)) {
......
} else if (isUndef(old)) {
......
} else if (cur !== old) {
// 更新的时候进入该逻辑
old.fns = cur
on[name] = old
}
}
// 遍历oldOn, 初始化的时候没有oldOn
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
function remove (
event: string,
handler: Function,
capture: boolean,
_target?: HTMLElement
) {
// 执行原生DOM操作方法移除事件
(_target || target).removeEventListener(
event,
handler._withTask || handler,
capture
)
}
更新的过程比较简单,判断新的DOM
方法与老的方法不相同的时候,会将老方法的fns
属性指向更新的方法,这也是为什么初始化事件方法的时候需要调用createFnInvoker
给函数做个包装。然后会遍历老节点上的方法,当新节点没有定义相同的方法的时候,移除该方法。
小结
event
的原生DOM
事件的生成和更新过程还是较为简单的,都是对编译过程中生成的event
进行处理,结合不同的修饰符,通过原生的DOM
操作方法将事件函数挂载到元素节点上。
下一节我们分析自定义事件的生成过程。