Vue 中的事件处理机制详解

一:vue中如何绑定事件?
vue事件分为两类,一个是原生dom事件,一个是组件自定义事件,绑定方法类似:

#绑定原生dom事件
<div id="example-1">
  <button v-on:click="handle">Add 1</button>
  <p>The button above has been clicked {
     {
      counter }} times.</p>
</div>
<div id="example-3">
  <button v-on:click="say('hi')">Say hi</button>
  <button v-on:click="say('what')">Say what</button>
</div>

#绑定自定义事件,通过组件内部 $emit('myEvent')触发
<my-component v-on:myEvent="doSomething"></my-component>

#在自定义组件上绑定原生事件
<my-component v-on:click.native="doSomething"></my-component>

#绑定动态事件,eventName为实例中能够访问到的变量
<my-component v-on[eventName]="doSomething"></my-component>
<my-component @[eventName]="doSomething"></my-component>

二:vue中的事件修饰符
dom原生事件往往我们需要的不只是绑定,我们还需要处理冒泡,捕获,取消默认事件等特殊场景。

  1. vue提供了非常便捷的事件修饰符来方便我们很简单的实现这些功能。
  • .stop (取消冒泡)
  • .prevent (取消默认事件)
  • .capture (捕获阶段执行)
  • .self (只有event.target 就是当前元素才执行)
  • .once (只执行一次,执行完就销毁)
  • .passive (滚动事件允许默认行为和scroll不阻塞执行)
  1. vue 还提供了按键修饰符来实现更多复杂的交互
  • .enter (回车触发)
  • .tab (tab键盘触发)
  • .delete (捕获“删除”和“退格”键)
  • .esc (esc键触发)
  • .space (空格键触发)
  • .up (向上的键触发)
  • .down(向上的键触发)
  • .left(向左的键触发)
  • .right(向右键触发)
  • .ctrl(ctrl键触发)
  • .alt(alt键触发)
  • .shift(shift键触发)
  • .meta(window键触发)
  • .left(鼠标左键触发)
  • .right(鼠标右键触发)
  • .middle(鼠标中键触发)
  1. 使用方法
<!-- 阻止单击事件继续传播 -->
<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>

具体大家可以自己去一一尝试,其中有一些按键是有系统兼容问题,大家参考文档注意处理。

三:核心源码解读
1.v-on 指令或者@on 实现

  • vue通过解析template里的html提取出dom上的所有属性
// 正则匹配 html字符串里的   a="xx" @a="xx" @click='xxx' v-on:click="xx" 等属性定义字符串  
 var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; 
 // 正则匹配 动态的属性写法  @[x]="handle1"    v-on[x]=""  :[x]="" 
 var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
  
function parseStartTag () {
     
      var start = html.match(startTagOpen);
      if (start) {
     
        var match = {
     
          tagName: start[1],
          attrs: [],
          start: index
        };
        advance(start[0].length);
        var end, attr;
        // 开始匹配,匹配后的属性压入到attrs里
        while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
      
          attr.start = index;
          advance(attr[0].length);
          attr.end = index;
          match.attrs.push(attr);
        }
        if (end) {
     
          match.unarySlash = end[1];
          advance(end[0].length);
          match.end = index;
          return match
        }
      }
    }

  • 通过正则匹配出对应的事件名和对应的事件执行方法
// 从属性中匹配和事件相关的属性
 var onRE = /^@|^v-on:/;
 // 匹配事件修饰符
 function parseModifiers (name) {
     
    var match = name.match(modifierRE);
    if (match) {
     
      var ret = {
     };
      match.forEach(function (m) {
      ret[m.slice(1)] = true; });
      return ret
    }
  }
 //省略N行代码
 if (onRE.test(name)) {
      // 匹配v-on或者@开头的属性
    name = name.replace(onRE, '');
     isDynamic = dynamicArgRE.test(name);
     if (isDynamic) {
     
       name = name.slice(1, -1);
     }
     addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic);
   }
   
// 生成渲染函数字符串
function addHandler (
    el,
    name,
    value,
    modifiers,
    important,
    warn,
    range,
    dynamic
  ) {
     
    modifiers = modifiers || emptyObject;//事件修饰符
    // warn prevent and passive modifier
    /* istanbul ignore if */
    if (
      warn &&
      modifiers.prevent && modifiers.passive // prevent 和passive 不能一起使用,两个是互斥的
    ) {
     
      warn(
        'passive and prevent can\'t be used together. ' +
        'Passive handler can\'t prevent default event.',
        range
      );
    }

    // normalize click.right and click.middle since they don't actually fire
    // this is technically browser-specific, but at least for now browsers are
    // the only target envs that have right/middle clicks.
    if (modifiers.right) {
     
      if (dynamic) {
     
        name = "(" + name + ")==='click'?'contextmenu':(" + name + ")";
      } else if (name === 'click') {
     
        name = 'contextmenu';
        delete modifiers.right;
      }
    } else if (modifiers.middle) {
     
      if (dynamic) {
     
        name = "(" + name + ")==='click'?'mouseup':(" + name + ")";
      } else if (name === 'click') {
     
        name = 'mouseup';
      }
    }

    // check capture modifier
    if (modifiers.capture) {
      // 处理捕获
      delete modifiers.capture;
      name = prependModifierMarker('!', name, dynamic);
    }
    if (modifiers.once) {
      // 处理只执行一次
      delete modifiers.once;
      name = prependModifierMarker('~', name, dynamic);
    }
    /* istanbul ignore if */
    if (modifiers.passive) {
      // 处理passive
      delete modifiers.passive;
      name = prependModifierMarker('&', name, dynamic);
    }

    var events;
    if (modifiers.native) {
      // 处理原生事件
      delete modifiers.native;
      events = el.nativeEvents || (el.nativeEvents = {
     });
    } else {
     
      events = el.events || (el.events = {
     });
    }

    var newHandler = rangeSetItem({
      value: value.trim(), dynamic: dynamic }, range);
    if (modifiers !== emptyObject) {
     
      newHandler.modifiers = modifiers;
    }

    var handlers = events[name];
    /* istanbul ignore if */
    if (Array.isArray(handlers)) {
     
      important ? handlers.unshift(newHandler) : handlers.push(newHandler);
    } else if (handlers) {
     
      events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
    } else {
     
      events[name] = newHandler;
    }

    el.plain = false;
  }
  

最终结果是el的events里维护了事件和事件对应的内容方法以及修饰符,以及是否是动态事件名等信息。
Vue 中的事件处理机制详解_第1张图片

  • 通过gen方法生成事件虚拟渲染函数
function genData$2 (el, state) {
     
    var data = '{';

   // 省略N行代码
    // event handlers
    if (el.events) {
     
      data += (genHandlers(el.events, false)) + ",";
    }
    if (el.nativeEvents) {
     
      data += (genHandlers(el.nativeEvents, true)) + ",";
    }
    // v-on data wrap
    if (el.wrapListeners) {
     
      data = el.wrapListeners(data);
    }
    return data
  }

效果如下
vue event gendata

  • 事件作为属性注入到虚拟dom 里
function createCompileToFunctionFn (compile) {
     
    var cache = Object.create(null);

    return function compileToFunctions (
      template,
      options,
      vm
    ) {
     
      options = extend({
     }, options);
    // 省略N行
      // check cache
      var key = options.delimiters
        ? String(options.delimiters) + template
        : template;
      if (cache[key]) {
     
        return cache[key]
      }

   // 省略N行
      // turn code into functions
      var res = {
     };
      var fnGenErrors = [];
      res.render = createFunction(compiled.render, fnGenErrors);
      res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
     
        return createFunction(code, fnGenErrors)
      });
// 省略N行代码
		// 转化后的渲染函数会缓存起来避免重复生成,生成的函数模版内容见下一个截图
      return (cache[key] = res) 
    }
  }
  // 将代码字符串转化为函数
function createFunction (code, errors) {
     
    try {
     
      return new Function(code)
    } catch (err) {
     
      errors.push({
      err: err, code: code });
      return noop
    }
  }

这里可以看到通过compile生成的虚拟树和render函数字符串。这是vue的核心之一。
AST和render

  • 虚拟dom转化到实际dom,并调用原生addEventListener绑定事件
# 进入挂载方法
Vue.prototype.$mount = function (
    el,
    hydrating
  ) {
     
    el = el && query(el);

    /* istanbul ignore if */
    if (el === document.body || el === document.documentElement) {
     
      warn(
        "Do not mount Vue to  or  - mount to normal elements instead."
      );
      return this
    }

    var options = this.$options;
    // resolve template/el and convert to render function
    if (!options.render) {
     
      var template = options.template;
      if (template) {
     
        if (typeof template === 'string') {
     
          if (template.charAt(0) === '#') {
     
            template = idToTemplate(template);
            /* istanbul ignore if */
            if (!template) {
     
              warn(
                ("Template element not found or is empty: " + (options.template)),
                this
              );
            }
          }
        } else if (template.nodeType) {
     
          template = template.innerHTML;
        } else {
     
          {
     
            warn('invalid template option:' + template, this);
          }
          return this
        }
      } else if (el) {
     
        template = getOuterHTML(el);
      }
      if (template) {
     
        /* istanbul ignore if */
        if (config.performance && mark) {
     
          mark('compile');
        }

        var ref = compileToFunctions(template, {
     
          outputSourceRange: "development" !== 'production',
          shouldDecodeNewlines: shouldDecodeNewlines,
          shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments
        }, this);
        var render = ref.render;
        var staticRenderFns = ref.staticRenderFns;
        options.render = render;
        options.staticRenderFns = staticRenderFns;

        /* istanbul ignore if */
        if (config.performance && mark) {
     
          mark('compile end');
          measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
        }
      }
    }
    return mount.call(this, el, hydrating)
  };
  // 实际挂载方法
 function mountComponent (
    vm,
    el,
    hydrating
  ) {
     
    vm.$el = el;
   //  省略N行
    } else {
     
      updateComponent = function () {
     
      // 执行模版更新,调用_render方法创建vnode,调用_update更新dom
        vm._update(vm._render(), hydrating); 
      };
    }

    // we set this to vm._watcher inside the watcher's constructor
    // since the watcher's initial patch may call $forceUpdate (e.g. inside child
    // component's mounted hook), which relies on vm._watcher being already defined
     // 创建watcher 来执行updateComponent,会初次执行一次
    new Watcher(vm, updateComponent, noop, {
      
      before: function before () {
     
        if (vm._isMounted && !vm._isDestroyed) {
     
          callHook(vm, 'beforeUpdate');
        }
      }
    }, true /* isRenderWatcher */); // 
    hydrating = false;

    // manually mounted instance, call mounted on self
    // mounted is called for render-created child components in its inserted hook
    if (vm.$vnode == null) {
     
      vm._isMounted = true;
      callHook(vm, 'mounted');
    }
    return vm
  }
// 事件绑定函数
function add$1 (
    name,
    handler,
    capture,
    passive
  ) {
     
    // async edge case #6566: inner click event triggers patch, event handler
    // attached to outer element during patch, and triggered again. This
    // happens because browsers fire microtask ticks between event propagation.
    // the solution is simple: we save the timestamp when a handler is attached,
    // and the handler would only fire if the event passed to it was fired
    // AFTER it was attached.
    if (useMicrotaskFix) {
     
      var attachedTimestamp = currentFlushTimestamp;
      var original = handler;
      handler = original._wrapper = function (e) {
     
        if (
          // no bubbling, should always fire.
          // this is just a safety net in case event.timeStamp is unreliable in
          // certain weird environments...
          e.target === e.currentTarget ||
          // event is fired after handler attachment
          e.timeStamp >= attachedTimestamp ||
          // bail for environments that have buggy event.timeStamp implementations
          // #9462 iOS 9 bug: event.timeStamp is 0 after history.pushState
          // #9681 QtWebEngine event.timeStamp is negative value
          e.timeStamp <= 0 ||
          // #9448 bail if event is fired in another document in a multi-page
          // electron/nw.js app, since event.timeStamp will be using a different
          // starting reference
          e.target.ownerDocument !== document
        ) {
     
          return original.apply(this, arguments)
        }
      };
    }
    target$1.addEventListener(// 最终在这里绑定
      name,
      handler,
      supportsPassive
        ? {
      capture: capture, passive: passive }
        : capture
    );
  }

四:vue如何优化事件?

vue在处理大列表绑定事件的时候,是有一定的性能问题的,框架内部没有把事件提到父节点上来做事件委托,唯一优化的是列表之间绑定的事件指向的函数都是同一个引用,且在dom销毁的时候能主动销毁事件,所以能负载一定的数据量,如果业务里的确存在非常大量的数据,建议还是自己在父节点上进行事件绑定,或者改变交互,进行分页。

你可能感兴趣的:(前端,vue)