前言
学习的时候无意中发现的,我也不知道这算不算一个值得学习的小操作,但是我认为这应该算是吧,如果有大佬觉得这点东西太low了的话,欢迎评论交流。。。。
vue事件绑定的大概流程
我要说的那个小操作是在
如果新旧节点上绑定的事件不一致了
这一步,如何将dom节点绑定的事件切换成最新的事件。
前置知识介绍
大家都知道在
Vue
中一个DOM
节点是可以绑定多个事件的,假如你写了一个这样的节点
经过模板编译生成
Vnode
之后,这个节点绑定的函数会被放在Vnode
的data
属性中的on
属性中,看起来是这样的。
然后在vue的
diff
阶段会传入两个新旧Vnode
节点,就进入了上面的那个对事件操作的流程,接下来我们就重点关心如果新旧节点上绑定的事件不一致了
这一步。
vue是如何通过这个小操作减少addEventListener的调用次数
大家先设想一下,如果让我们自己来设置这个事件绑定功能的话,已经拿到了上面的每个节点的事件对应绑定的方法了,我们自己写的话应该会是这样吧。
function updateListeners(
on, //新Vnode节点里的on属性
oldOn, //旧Vnode节点里的on属性
add, //给Vnode节点对应的真实节点添加事件绑定的函数
remove, //给Vnode节点对应的真实节点解除事件绑定的函数
vm //vue实例,一般作为函数执行的上下文使用
){
let name, cur, old, event
for(name in on) { //循环新节点所有的事件名称
cur = on[name] //拿到新节点事件对应要绑定的函数
old = oldOn[name] //拿到旧节点事件对应要绑定的函数
if(old == undefined) { //旧节点上没有这个事件
//直接在真实的dom上添加这个事件
add(cur,name)
} else if(cur !== old){ //新旧节点绑定的事件不一致了,将旧事件切换成新事件
//先remove事件,然后绑定新的事件
remove(cur,name)
add(cur,name)
}
}
for(name in oldOn) { //循环旧节点所有的事件名称
if(on[name] == undefined) { //如果新节点没有绑定这个事件了
remove(name, oldOn[name]) //在真实dom上移除这个节点
}
}
}
正常来说的话,基本上大致就是这个流程就能完成这个事件绑定的功能了,但是vue做了一个处理,对于用户传入的函数外面包了一层函数,实现这个包一层函数的这个函数是我觉得很巧妙的一个地方,因为有了这个函数之后,如果新旧节点的事件绑定的函数发生了变化,就不需要再调用重量级的
addEventListener
了
export function createFnInvoker(fns, vm) {
function invoker () {
const fns = invoker.fns //注意这里,每次事件触发时,激活的函数是从外部读取的
if(Array.isArray(fns)) { //从这里看出来vue是支持一个行为绑定多个函数的
for(let i = 0;i<fns.length;i++) {
fns[i].call(vm)
}
} else {
fns.call(vm) //vue在这里其实又套了一层函数,是为了捕获函数运行过程中出的错误
}
}
invoker.fns = fns //这里就很灵魂了
return invoker
}
经过这样的处理之后,如果我们给一个
DOM
节点绑定了上面这个createFnInvoker
函数处理了之后的函数,假如函数发生了变化,我们就不需要再重新addEventListener
而是将绑定的那个函数的fns属性切换成新的函数
就可以了,因为每次激活这个事件绑定的函数的时候,是会先读取fns
然后执行
的。
最后看一下处理事件的主体函数的简化代码吧
function updateListeners(
on, //新Vnode节点里的on属性
oldOn, //旧Vnode节点里的on属性
add, //给Vnode节点对应的真实节点添加事件绑定的函数
remove, //给Vnode节点对应的真实节点解除事件绑定的函数
vm //vue实例,一般作为函数执行的上下文使用
){
let name, cur, old, event
for(name in on) { //循环新定义的事件
cur = on[name]
old = oldOn[name];
event = normalizeEvent(name)
if(isUndef(old)) { //新事件未定义,直接在新节点上定义
if(isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm)
}
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)
}
}
}
小结
我也不知道这算不算某种模式或者啥的,我只是觉得这个小操作值得学习,不一定非要是这样的场景,其他类似的场景如果有频繁的事件切换的话,可以通过这种方式来巧妙地
“避重就轻”
有啥问题的话欢迎一起讨论!