zoukankan      html  css  js  c++  java
  • VUE源码——事件机制

    VUE是怎么样处理事件的

    在日常的开发中,我们把 @click 用的飞起,组件自定义事件实现父子组件之间的通信,那我们有想过其中的实现原理是什么呢?接下来我们将探索原生事件和自定义事件的奥秘。带着疑问开始撸源码。

    首先来点儿测试代码,在测试代码中,我们包含了原生的事件,和自定义事件

    <body>
      <div id="app">
        <h1>事件处理</h1>
        <!-- 普通事件 -->
        <p @click='onclick'>普通事件</p>
        <!-- 自定义事件 -->
        <comp @myclick="onMyClick"></comp>
      </div>
    </body>
    <script>
      Vue.component('comp', {
        template: `<div @click="onClick">this is comp</div>`,
        methods: {
          onClick() {
            this.$emit('myclick')
          }
        }
      })
      const app = new Vue({
        el: '#app',
        methods: {
          onclick() {
            console.log('普通事件');
          },
          onMyClick() {
            console.log('自定义事件');
          }
        },
      })
      console.log(app.$options.render);
    </script>

    在Vue 挂载之前做了许多编译的工作,把 template 模板编译成 render函数,这个过程就不做过多的讲解。我们主要来看生产render函数后是怎么实现事件的绑定的。

    我们来观察打印出的app.$options.render 的结果

    (function anonymous() {
        with(this) {
          return _c('div', {
            attrs: {
              "id": "app"
            }
          }, [_c('h1', [_v("事件处理")]), _v(" "), _c('div', {
            on: {
              "click": onclick
            }
          }, [_v("普通事件")]), _v(" "), _c('comp', {
            on: {
              "myclick": onMyClick
            }
          })], 1)
        }
      })

    根据打印的结果来看,普通事件和自定义事件生成的结果其实差不多,都将事件的处理放在了on上面。

    普通事件

    据我所知,在Vue 组件初始化的时候,原生事件的监听会在 platformsweb untimemodulesevents.js里面,会执行 updateDOMListeners方法。

    想要知道验证一下,是否执行到了该函数,我们可以在函数里面打断点验证一下。 

    可以看到,我们会成功的进入,那想要知道调用流程,我们可以在堆栈信息里面看看。

    因为在有了 Vnode 过后,会遍历子节点递归的调用 createElm 为每个子节点创建真实的 DOM,在创建真实的 DOM 时会组成相关的钩子invokeCreateHooks。其中就包括注册事件的处理 updateDOMListeners 

     进入到 invokeCreateHooks 函数

    function invokeCreateHooks (vnode, insertedVnodeQueue) {
      for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
        cbs.create[i$1](emptyNode, vnode);
      }
      i = vnode.data.hook; // Reuse variable
      if (isDef(i)) {
        if (isDef(i.create)) { i.create(emptyNode, vnode); }
        if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
      }
    }

    我们可以看看会执行哪些钩子函数。

     我们可以看到,在 invokeCreateHooks 函数里面,是把所有的钩子函数执行一遍,其中就有 updateDOMListeners 

    function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
      if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
        return
      }
      const on = vnode.data.on || {}
      const oldOn = oldVnode.data.on || {}
      target = vnode.elm
      // 兼容性处理
      normalizeEvents(on)
      updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
      target = undefined
    }

    其中 normalizeEvents 是对 v-model 的兼容性处理,在 IE 下没有 input 只支持 change 事件,把 input 事件替换成 change 事件。

    if (isDef(on[RANGE_TOKEN])) {
        // IE input[type=range] only supports `change` event
        const event = isIE ? 'change' : 'input'
        on[event] = [].concat(on[RANGE_TOKEN], on[event] || [])
        delete on[RANGE_TOKEN]
      }

    updateListeners 的逻辑也不复杂,它会遍历on事件对新节点事件绑定注册事件,对旧节点移除事件监听。

    export function updateListeners (
      on: Object,
      oldOn: Object,
      add: Function,
      remove: Function,
      createOnceHandler: Function,
      vm: Component
    ) {
      let name, def, cur, old, event
      for (name in on) {
        ...
        //  执行真正注册事件的执行函数
          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)
        }
      }
    }

    add 函数,是在真正的 DOM 上绑定事件,它的实现也是利用了原生 DOM 的 addEventListener

    function add (
      name: string,
      handler: Function,
      capture: boolean,
      passive: boolean
    ) {
      ...
      
      target.addEventListener(
        name,
        handler,
        supportsPassive
          ? { capture, passive }
          : capture
      )
    }

    一目了然,将事件添加到原生的click事件上,并实现了监听。 以上就是普通事件绑定的流程。

    自定义事件

    我们知道,父子组件可以使用事件进行通信,子组件通过vm.$emit 向父组件派发事件,父组件通过v-on:(event)接受信息并处理回调。

    从最开始的例子中可以看出,普通节点使用的原生DOM事件,在组件上可以使用自定义事件,另外组件上还可以使用原生事件,用 .native 修饰符区分。 接下来我们看看自定义事件是怎么处理的。

    Vnode 生成真实节点的过程中,这个过程遇到子Vnode会实例化子组件实例。实例化子类构造器的过程,会有初始化选项配置的过程,会进入到Vue.prototype.init,我们直接看对自定义事件的处理。 在 srccoreinstanceinit.js

    Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // a uid
        vm._uid = uid++
    
        ...
        // merge options
        // 针对子组件的事件处理
        if (options && options._isComponent) {
          // optimize internal component instantiation
          // since dynamic options merging is pretty slow, and none of the
          // internal component options needs special treatment.
          initInternalComponent(vm, options)
        } else {
          vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
        /* istanbul ignore else */
        if (process.env.NODE_ENV !== 'production') {
          initProxy(vm)
        } else {
          vm._renderProxy = vm
        }
        // expose real self
        vm._self = vm
        initLifecycle(vm)
        // 初始化事件处理
        initEvents(vm)
        initRender(vm)
        callHook(vm, 'beforeCreate')
        initInjections(vm) // resolve injections before data/props
        initState(vm)
        initProvide(vm) // resolve provide after data/props
        callHook(vm, 'created')
    
        ...
      }

    进入到事件处理函数 initEvents, 里面的处理逻辑也是比较简单,就几行代码。

    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)
      }
    }

    我们把断点打到函数里面看一看 

     第一次进去的时候,我们看到当前创建的是根组件,根组件的 _uid:0, 我们放过再进一次,现在看到的就是我们自定义的组件在创建。这时候的 listeners 会存在。

     接下来会进去 updateComponentListeners,自定义事件的处理。

    export function updateComponentListeners (
      vm: Component,
      listeners: Object,
      oldListeners: ?Object
    ) {
      target = vm
      updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
      target = undefined
    }

    简单的看这段代码,把当前组件实例赋值给目标对象 target, 然后进行事件监听。

    同样的,会有 add 函数执行,那这里的 add 和原生事件的又所不同,我们可以猜想一下,这里的 add 是怎么处理的。

    export function updateListeners (
      on: Object,
      oldOn: Object,
      add: Function,
      remove: Function,
      createOnceHandler: Function,
      vm: Component
    ) {
      let name, def, cur, old, event
      for (name in on) {
        ...
        } 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)
        }
      }
    }

    可能也猜想到了,是通过 $on 进行事件监听

    function add (event, fn) {
      target.$on(event, fn)
    }

    我们可以看到,自定义事件,虽然是在事件监听声明在父组件上来,但是监听还是在子组件上监听的,所义谁派发,谁监听。

    那会存在疑问,自己派发,自己监听,那是怎么和父组件经通信的呢? 这里需要注意下,回调函数是在父组件声明的。

    我们会想,子组件是怎么拿到父组件的自定义事件的呢 ,其实在updateComponentListeners  vm.$options._parentListeners,可以拿到父组件的自定义事件。那么 _parentListeners 又是怎么来的呢?

    其实在 _init 方法里,执行 initEvents 之前,会对组件进行处理。initInternalComponent(vm, options)

    export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
      const opts = vm.$options = Object.create(vm.constructor.options)
      // doing this because it's faster than dynamic enumeration.
      const parentVnode = options._parentVnode
      opts.parent = options.parent
      opts._parentVnode = parentVnode
    
      const vnodeComponentOptions = parentVnode.componentOptions
      opts.propsData = vnodeComponentOptions.propsData
      opts._parentListeners = vnodeComponentOptions.listeners
      opts._renderChildren = vnodeComponentOptions.children
      opts._componentTag = vnodeComponentOptions.tag
    
      if (options.render) {
        opts.render = options.render
        opts.staticRenderFns = options.staticRenderFns
      }
    }

    在父组件里面的组件的 vnodeComponentOptions里面的 listeners就是自定义组件里面定义的事件,myClick, 这样在子组件内部就可以拿到,然后在绑定在 on 事件上。

    总结

    在模板编译阶段会以属性的形式存在,在真实节点渲染阶段会根据事件属性去绑定相关的事件。对于组件的自定义事件来说,我们可以用事件进行父子组件间的通信,其实质是在子组件内部自己派发事件,监听事件。能达到通信的效果,是因为回调函数是在父组件中声明的。

  • 相关阅读:
    Channel
    MemCache
    算法笔记_124:密码脱落(Java)
    算法笔记_123:蓝桥杯第七届省赛(Java语言B组部分习题)试题解答
    算法笔记_122:蓝桥杯第七届省赛(Java语言A组)试题解答
    算法笔记_121:蓝桥杯第六届省赛(Java语言C组部分习题)试题解答
    算法笔记_120:蓝桥杯第六届省赛(Java语言B组部分习题)试题解答
    算法笔记_119:蓝桥杯第六届省赛(Java语言A组)试题解答
    算法笔记_118:算法集训之结果填空题集二(Java)
    算法笔记_117:算法集训之结果填空题集一(Java)
  • 原文地址:https://www.cnblogs.com/DivHao/p/11806322.html
Copyright © 2011-2022 走看看