分享一个vue2.x源码中,事件绑定的一个值得学习的小操作。

989 阅读4分钟

前言


学习的时候无意中发现的,我也不知道这算不算一个值得学习的小操作,但是我认为这应该算是吧,如果有大佬觉得这点东西太low了的话,欢迎评论交流。。。。

vue事件绑定的大概流程

我要说的那个小操作是在如果新旧节点上绑定的事件不一致了这一步,如何将dom节点绑定的事件切换成最新的事件。

前置知识介绍

大家都知道在Vue中一个DOM节点是可以绑定多个事件的,假如你写了一个这样的节点

经过模板编译生成Vnode之后,这个节点绑定的函数会被放在Vnodedata 属性中的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)
        }
    }
}

小结

我也不知道这算不算某种模式或者啥的,我只是觉得这个小操作值得学习,不一定非要是这样的场景,其他类似的场景如果有频繁的事件切换的话,可以通过这种方式来巧妙地“避重就轻”

有啥问题的话欢迎一起讨论!