【CSDN软件工程师能力认证学习精选】Vue 中的事件处理机制详解

CSDN软件工程师能力认证(以下简称C系列认证)是由中国软件开发者网CSDN制定并推出的一个能力认证标准。C系列认证历经近一年的实际线下调研、考察、迭代、测试,并梳理出软件工程师开发过程中所需的各项技术技能,结合企业招聘需求和人才应聘痛点,基于公开、透明、公正的原则,甑别人才时确保真实业务场景、全部上机实操、所有过程留痕、存档不可篡改。

我们每天将都会精选CSDN站内技术文章供大家学习,帮助大家系统化学习IT技术。

一: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原生事件往往我们需要的不只是绑定,我们还需要处理冒泡,捕获,取消默认事件等特殊场景。

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

3、使用方法

<!-- 阻止单击事件继续传播 -->
<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里维护了事件和事件对应的内容方法以及修饰符,以及是否是动态事件名等信息。


通过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
  }


效果如下


事件作为属性注入到虚拟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的核心之一。


虚拟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 <html> or <body> - 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销毁的时候能主动销毁事件,所以能负载一定的数据量,如果业务里的确存在非常大量的数据,建议还是自己在父节点上进行事件绑定,或者改变交互,进行分页。
————————————————

关于CSDN软件工程师能力认证

      CSDN软件工程师能力认证(以下简称C系列认证)是由中国软件开发者网CSDN制定并推出的一个能力认证标准。C系列认证历经近一年的实际线下调研、考察、迭代、测试,并梳理出软件工程师开发过程中所需的各项技术技能,结合企业招聘需求和人才应聘痛点,基于公开、透明、公正的原则,甑别人才时确保真实业务场景、全部上机实操、所有过程留痕、存档不可篡改。C系列认证的宗旨是让一流的技术人才凭真才实学进大厂拿高薪,同时为企业节约大量招聘与培养成本,使命是提升高校大学生的技术能力,为行业提供人才储备,为国家数字化战略贡献力量。

 

了解详情可点击:CSDN软件工程师能力认证介绍

 

 

原文链接:https://blog.csdn.net/weixin_41275295/article/details/100549145

已标记关键词 清除标记
相关推荐