Vue2总结(七)Vue事件机制

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> Vue2总结(七)Vue事件机制

目前社区有很多 Vue.js 的源码解读文章,但是质量层次不齐,不够系统和全面,最近我将从各个细枝末节解读 Vue.js 的实现原理,针对 vue3 版本之前,让同学们可以彻底掌握 Vue.js。 友情提示:阅读本文大概需要** 30****分钟**

前言

我们平时开发工作中,处理组件间的通讯,原生的交互,都离不开事件。通过官网源码的解读得知,Vue 支持 2 种事件类型,原生 DOM 事件和自定义事件,它们主要的区别在于添加和删除事件的方式不一样,并且自定义事件的派发是往当前实例上派发,但是可以利用在父组件环境定义回调函数来实现父子组件的通讯。对于一个组件元素,我们不仅仅可以绑定原生的 DOM 事件,还可以绑定自定义事件,非常灵活和方便。那么接下来将具体分析一下 Vue 的事件监听机制。

Vue事件机制

Vue定义了四种添加事件监听的方法,EventsMixin:

  • `$on`   监听事件
  • `$once` 只监听一次
  • `$off`  移除事件
  • `$emit`  自定义事件
  • Vue事件API

    Vue 事件修饰符及其他在官网已有详细说明,这里只讨论这四种事件监听。在Vue3之前,Vue提供了四个事件API,分别是 $on $once $off $emit

    
    !-- 阻止单击事件继续传播 --
    a v-on:click.stop="doThis"/a
    
    !-- 提交事件不再重载页面 --
    form v-on:submit.prevent="onSubmit"/form
    
    !-- 修饰符可以串联 --
    a v-on:click.stop.prevent="doThat"/a
    
    !-- 只有修饰符 --
    form v-on:submit.prevent/form
    
    !-- 添加事件监听器时使用事件捕获模式 --
    !-- 即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理 --
    div v-on:click.capture="doThis".../div
    
    !-- 只当在 event.target 是当前元素自身时触发处理函数 --
    !-- 即事件不是从内部元素触发的 --
    div v-on:click.self="doThat".../div
    

    初始化事件

    初始化事件在vm上创建一个_events对象,用来存放事件。_events的内容如下:

    
    {
        eventName: [function1, function2, function3]
    }
    

    存放事件名以及对应执行方法。

    
    /*初始化事件*/
    export function initEvents (vm: Component) {
      /*在vm上创建一个_events对象,用来存放事件。*/
      vm._events = Object.create(null)
      /*这个bool标志位来表明是否存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。*/
      vm._hasHookEvent = false
      // init parent attached events
      /*初始化父组件attach的事件*/
      const listeners = vm.$options._parentListeners
      if (listeners) {
        updateComponentListeners(vm, listeners)
      }
    }
    

    $on

    $on方法用来在 vm 实例上监听一个自定义事件,该事件可用emit 触发。

    
      Vue.prototype.$on = function (event: string | Arraystring, fn: Function): Component {
        const vm: Component = this
    
        /*如果是数组的时候,则递归$on,为每一个成员都绑定上方法*/
        if (Array.isArray(event)) {
          for (let i = 0, l = event.length; i  l; i++) {
            this.$on(event[i], fn)
          }
        } else {
          (vm._events[event] || (vm._events[event] = [])).push(fn)
          // optimize hook:event cost by using a boolean flag marked at registration
          // instead of a hash lookup
          /*这里在注册事件的时候标记bool值也就是个标志位来表明存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。*/
          if (hookRE.test(event)) {
            vm._hasHookEvent = true
          }
        }
        return vm
      }
    

    $once

    
    $once</code>监听一个只能触发一次的事件,在触发以后会自动移除该事件。</p><pre style="box-sizing: border-box;font-size: 14px;color: rgb(62, 62, 62);line-height: inherit;text-align: start;background-color: rgb(255, 255, 255);"><code class="hljs cs" style="box-sizing: border-box;margin-right: 2px;margin-left: 2px;padding: 0.5em;color: rgb(220, 220, 220);line-height: 18px;border-radius: 0px;background: rgb(63, 63, 63);font-family: Consolas, Inconsolata, Courier, monospace;display: block;overflow-x: auto;letter-spacing: 0px;overflow-wrap: normal !important;word-break: normal !important;overflow-y: auto !important;">  Vue.prototype.$once = function (event: string, fn: Function): Component {
        const vm: Component = this
        function on () {
          /*在第一次执行的时候将该事件销毁*/
          vm.$off(event, on)
          /*执行注册的方法*/
          fn.apply(vm, arguments)
        }
        on.fn = fn
        vm.$on(event, on)
        return vm
      }
    

    $off

    $off用来移除自定义事件

    
    Vue.prototype.$off = function (event?: string | Arraystring, fn?: Function): Component {
        const vm: Component = this
        // all
        /*如果不传参数则注销所有事件*/
        if (!arguments.length) {
          vm._events = Object.create(null)
          return vm
        }
        // array of events
        /*如果event是数组则递归注销事件*/
        if (Array.isArray(event)) {
          for (let i = 0, l = event.length; i  l; i++) {
            this.$off(event[i], fn)
          }
          return vm
        }
        // specific event
        const cbs = vm._events[event]
        /*Github:https://github.com/answershuto*/
        /*本身不存在该事件则直接返回*/
        if (!cbs) {
          return vm
        }
        /*如果只传了event参数则注销该event方法下的所有方法*/
        if (arguments.length === 1) {
          vm._events[event] = null
          return vm
        }
        // specific handler
        /*遍历寻找对应方法并删除*/
        let cb
        let i = cbs.length
        while (i--) {
          cb = cbs[i]
          if (cb === fn || cb.fn === fn) {
            cbs.splice(i, 1)
            break
          }
        }
        return vm
      }
    

    $emit

    $emit用来触发指定的自定义事件。

    
    Vue.prototype.$emit = function (event: string): Component {
        const vm: Component = this
        if (process.env.NODE_ENV !== 'production') {
          const lowerCaseEvent = event.toLowerCase()
          if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
            tip(
              `Event "${lowerCaseEvent}" is emitted in component ` +
              `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
              `Note that HTML attributes are case-insensitive and you cannot use ` +
              `v-on to listen to camelCase events when using in-DOM templates. ` +
              `You should probably use "${hyphenate(event)}" instead of "${event}".`
            )
          }
        }
        let cbs = vm._events[event]
        if (cbs) {
          /*将类数组的对象转换成数组*/
          cbs = cbs.length  1 ? toArray(cbs) : cbs
          const args = toArray(arguments, 1)
          /*遍历执行*/
          for (let i = 0, l = cbs.length; i  l; i++) {
            cbs[i].apply(vm, args)
          }
        }
        return vm
      }
    

    HTML元素上点击事件

    用 v-on 指令监听 DOM 事件,并在触发时运行一些JS代码,也可以通过@简写的方式,原因:Vue定义了这样的正则表达式 var onRE = /^@|^v-on:/;,在 processAttrs() 解析属性时通过onRE.test(name) 判断来添加属性。

    
    // Html 代码
    body
    div id="app"
        button @click="test"测试/button
    /div
    script
        new Vue({
            'el':'#app',
            methods:{
                test:function(){
                    alert('测试按钮被点击了');
                }
            }
        });
    /script
    /body
    

    Vue 初始化时,调用 initEvents(vm) 对事件进行初始化:

    
    function initEvents (vm) {
      vm._events = Object.create(null);
      //_hasHookEvent标志位表明是否存在钩子,而不需要通过哈希表来查找是否有钩子,这样做可以减少不必要的开销,优化性能。
      vm._hasHookEvent = false;
      //初始化父组件attach的事件
      var listeners = vm.$options._parentListeners;
      if (listeners) {
        updateComponentListeners(vm, listeners);
      }
    }
    

    initEvents方法在vm上创建一个_events对象,用来存放事件。接下来就调用initState()方法初始化事件,相关代码段:

    
    if (opts.methods) { initMethods(vm, opts.methods); }
    

    这个例子 methods 内有方法 test,因此会执行initMethods。

    
    vm[key] = methods[key] == null ? noop : bind(methods[key], vm);
    

    initMethods 遍历定义的 methods,通过调用 bind 改变函数的 this 指向,修饰了事件的回调函数,组件上挂载的事件都是在父作用域中的。

    
    function nativeBind (fn, ctx) {
      //fn的this指向ctx
      return fn.bind(ctx)
    }
    var bind = Function.prototype.bind
             ? nativeBind
             : polyfillBind;
    

    接下来 Vue 将 html 解析成 AST,接下来调用 add$1 通过addEventListener 将事件绑定到 target上。

    
    //
    function add$1 (event,handler,once$$1,apture,passive) {
      handler = withMacroTask(handler);
      if (once$$1) { handler = createOnceHandler(handler, event, capture); }
      //绑定点击事件
      target$1.addEventListener(
        event,
        handler,
        supportsPassive
          ? { capture: capture, passive: passive }
          : capture
      );
    }
    

    component上点击事件

    
    body
    div id="app"
        child-test :test-message="mess" v-on:click.native="test01" v-on:componenton="test02"/child-test
    /div
    template id="tplt01"
        button{{testMessage}}/button
    /template
    script
        new Vue({
            'el':'#app',
            data:{mess:'组件点击001'},
            methods:{
                test01:function(){
                    alert('01号测试按钮被点击');
                },
                test02:function(){
                    alert('01号测试按钮被点击!');
                }
            },
            components:{
                'childTest':{
                    template:"#tplt01",
                    props:['testMessage'],
                    methods:{
                        test02:function(){
                            alert('001号测试按钮被点击');
                        }
                    },
                }
            }
        });
    /script
    /body
    

    标签定义了** click 事件,若click事件没加修饰符.native,点击按钮不出发任何事件,若添加了 .native 修饰符,点击按钮执行的就是test01,alert(“01号被点了!”);。.native**的作用就是在原生 DOM 上绑定事件。

    不添加 .native的的事件解析过程与上文相同,而添加了**.native**之后,v-on的事件会被放到 nativeOn 数组中,随后被解析成 render。在事件初始化,调用 genHandlers 的时候,会先判断该事件是否为 native,如果是,解析的事件字符串就会用”nativeOn{}” 包裹。

    
    function genHandlers (
      events,
      isNative,
      warn
    ) {
      var res = isNative ? 'nativeOn:{' : 'on:{';
      for (var name in events) {
        res += """ + name + "":" + (genHandler(name, events[name])) + ",";
      }
      return res.slice(0, -1) + '}'
    }
    

    html解析成vnode之后会调用createComponent进行处理。

    
    function createComponent (
      Ctor,
      data,
      context,
      children,
      tag
    ) {
      if (isUndef(Ctor)) {
        return
      }
      var baseCtor = context.$options._base;
      /*其他代码省略*/
      //缓存data.on的函数,这些需要作为子组件监听器而不是DOM监听器来处理。就是componenton事件
      var listeners = data.on;
      //data.on被native修饰符的事件所替换
      data.on = data.nativeOn;
      /*其他代码省略*/
      var vnode = new VNode(
        ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
        data, undefined, undefined, undefined, context,
        { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
        asyncFactory
      );
      return vnode
    }
    

    createComponent将.native修饰符的事件放在data.on上面。接下来data.on上的事件(本文中的alert(‘001号测试按钮被点击’);) 会按普通的html事件往下走。

    
    function updateDOMListeners (oldVnode, vnode) {
      if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
        return
      }
      var on = vnode.data.on || {};
      var oldOn = oldVnode.data.on || {};
      target$1 = vnode.elm;
      normalizeEvents(on);
      updateListeners(on, oldOn, add$1, remove$2, vnode.context);
      target$1 = undefined;
    }
    

    最终通过 target$1.addEventListener添加事件监听。而标签内没有.native的修饰符调用的是 $on方法。

    
    function updateComponentListeners (
      vm,
      listeners,
      oldListeners
    ) {
      target = vm;
      updateListeners(listeners, oldListeners || {}, add, remove$1, vm);
      target = undefined;
    }
    

    updateListeners又调用了add方法

    
    function add (event, fn, once) {
      if (once) {
        target.$once(event, fn);
      } else {
        target.$on(event, fn);
      }
    }
    

    也就是说对于普通html元素和在组件标签内添加了.native修饰符的事件,都通过 target$1.addEventListener()来挂载事件。而定义在组件上的事件会调用原型上的 $on等方法。

    总结

    上面从源码里扯了那么多代码,可能还是云里雾里的,既然是4种事件监听,这里用class模拟一下Vue的事件机制,算是对文章的总结吧。

    
    class Event{
        constructor() {
            this._events = Object.create(null);
        }
    
        $on(event,fn) {
            if(Array.isArray(event)) {  //如果传的是数组就递归为每个成员绑定方法
                for(let i = 0; i  event.length; i++) {
                    this.$on(event[i],fn);
                }
            }else {
                (this._events[event] || (this._events[event] = [])).push(fn);
            }
        }
    
        $off(event,fn) {
            if (!arguments.length) {  //没传参数则全部销毁、
                this._events = Object.create(null);
                return
            }
            if (Array.isArray(event)) { //数组则递归销毁
                for(let i = 0; i  event.length; i++) {
                    this.$off(event[i],fn);
                }
                return
            }
            // 特殊处理event
            const cbs = this._events[event];
            if (!cbs) { //如果不存在就返回
                return
            }
            if(arguments.length === 1) { //如果只传了event参数,则销毁event下的所有方法
                this._events[event] = null;
                return
            }
            // 特殊处理fn
            // 遍历所有方法找到对应方法删除 
            let cb;
            let i = cbs.length - 1;
            while(i = 0) {
                cb = cbs[i];
                if (cb === fn || cb.fn === fn) { 
                    cbs.splice(i,1)
                    break;
                }
                i --
            }
        }
        $once(event,fn) {
            function on () {
                //每次执行事件先销毁
                this.$off(event,on);
                //执行handler
                fn.apply(this,arguments)
            }
            this.$on(event,on);
        }
    
        $emit(event,arg) {
            let cbs = this._events[event];
            let args = Array.from(Array.prototype.splice.call(arguments,1,arguments.length-1));
            if (cbs) {
                cbs = cbs.length  1 ? Array.from(cbs) : cbs;
                for (let i = 0; i  cbs.length; i++) {
                    cbs[i].apply(this,args);
                }
            }
        }
    }
    

    最后

    今天的 Vue2总结(七)事件机制 就分享到这里,我的公众号没有留言功能哈,有问题大家心里默念,我能感受到,谢谢 ~ 

    原文始发于微信公众号(程序员思语):

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> Vue2总结(七)Vue事件机制


     上一篇
    Vue2总结(八)Template模版编译 Vue2总结(八)Template模版编译
    目前社区有很多 Vue.js 的源码解读文章,但是质量层次不齐,不够系统和全面,最近我将从各个细枝末节解读 Vue.js 的实现原理,针对 vue3 版本之前,让同学们可以彻底掌握 Vue.js。 友情提示:阅读本文大概需要** 30*
    2021-04-05
    下一篇 
    Vue2总结(六)Flow静态类型检查 Vue2总结(六)Flow静态类型检查
    目前社区有很多 Vue.js 的源码解读文章,但是质量层次不齐,不够系统和全面,最近我将从各个细枝末节解读 Vue.js 的实现原理,针对 vue3 版本之前,让同学们可以彻底掌握 Vue.js。 友情提示:阅读本文大概需要** 23*
    2021-04-05