知识体系:Vue的事件机制

Vue的事件可以分为原生的DOM事件和自定义的事件
原生的DOM事件:其实就是通过addEventListener添加绑定事件,而我们通过v-on, @添加的事件都会先经过Vue的模板编译,生成抽象语法树(ast),再生成render函数,通过render函数生成我们的Vnode,render函数上的参数,就会告诉Vue我们最终生成真实的DOM有哪些属性,是否是有事件的
我自己写了一个例子,然后使用完整版的Vue(就是包含编译部分),看了一下生成render函数的过程,而使用只包含运行时的Vue,脏活累活vue-loader已经都帮我们做好了。这里我用完整版的
main.js

import Vue from 'vue'

Vue.config.productionTip = false

new Vue({
  el:"#app",
  template:`<div id="apps">
  <input type="text" v-model="inputValue"/>
  <div @click="clickFn">绑定</div>
  <div @click="clickFn2">发送</div>
  </div>`,
  data:function(){
    return {
      inputValue:"我是你爸爸"
    }
  },
  methods:{
    clickFn:function(){
     console.log("点击事件1")
      })
    },
    clickFn2:function(){
     console.log("点击事件2")
    }
  },
})

这是我的main.js,不需要配置render函数,我们通过debugger会依次执行 $mount->compileToFunctions->compile->
baseCompile

baseCompile源码

var createCompiler = createCompilerCreator(function baseCompile (
  template,
  options
) {
  var ast = parse(template.trim(), options);
  if (options.optimize !== false) {
    optimize(ast, options);
  }
  var code = generate(ast, options);
  return {
    ast: ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
});

这里代码就把整个大致的流程体现出来了,通过parse函数将我们的模板,转化成ast抽象语法树,再通过generate将ast树转化为render函数字符串
这个就是我们最总生成的render函数字符串,最终Vue会通过new Function()转成函数,挂载到当前Vue实例的render属性上

"with(this){
return _c('div',{attrs:{"id":"apps"}},
[
_c('input',{
		directives:[{name:"model",rawName:"v-model",value:(inputValue),expression:"inputValue"}],
		attrs:{"type":"text"},domProps:{"value":(inputValue)},
		on:{
		"input":function($event){if($event.target.composing)return;inputValue=$event.target.value}
		}
	}),
_v(" "),
_c('div',{on:{"click":clickFn}},[_v("绑定")]),
_v(" "),
_c('div',{on:{"click":clickFn2}},[_v("发送")])
])
}"

我大致整理的了一下,我们可以看到我们的三个dom转成的render函数字符串
v-mode属性:我们的render函数的on对象中会添加一个input参数,以及还会有一个directives属性(用来做后续的处理,会绑定额外的事件)
而通过@click绑定的事件,我们的render函数的on对象中会添加一个click属性
之后我们就会进入Vnode的创建和真实dom的创建和插入
每一个DOM的创建都会经过updateListeners这个函数,看他是否有要绑定的事件,他会遍历on对象中的属性,这个on对象就是我们上面render函数生成的属性

function updateListeners (
  on,
  oldOn,
  add,
  remove$$1,
  createOnceHandler,
  vm
) {
  var name, def$$1, cur, old, event;
 //遍历on 中的每一个属性   遍历on 中的每一个属性   遍历on 中的每一个属性   遍历on 中的每一个属性
  for (name in on) {
    def$$1 = cur = on[name];
    old = oldOn[name];
    event = normalizeEvent(name);
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        "Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
        vm
      );
    } 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$$1(event.name, oldOn[name], event.capture);
    }
  }
}

第一次执行的时候会进入add函数,之后的更新还会比较一下新旧的事件是不是一样的,并不是每次都会绑定事件
add(event.name, cur, event.capture, event.passive, event.params);

function add$1 (
  name,
  handler,
  capture,
  passive
) {
  if (useMicrotaskFix) {
    var attachedTimestamp = currentFlushTimestamp;
    var original = handler;
    handler = original._wrapper = function (e) {
      if (
        e.target === e.currentTarget ||
        e.timeStamp >= attachedTimestamp ||
        e.timeStamp <= 0 ||
        e.target.ownerDocument !== document
      ) {
        return original.apply(this, arguments)
      }
    };
  }
  target$1.addEventListener(
    name,
    handler,
    supportsPassive
      ? { capture: capture, passive: passive }
      : capture
  );
}

最下面 target$1.addEventListener就会为这个DOM绑定上事件,target$1是我们当前的节点,这个Vue在执行之前就已经赋好值的。
如果是v-model在DOM创建好之后,如果有directives(这个也是render函数生成的属性),还有进入这个inserted函数

inserted: function inserted (el, binding, vnode, oldVnode) {
    if (vnode.tag === 'select') {
      if (oldVnode.elm && !oldVnode.elm._vOptions) {
        mergeVNodeHook(vnode, 'postpatch', function () {
          directive.componentUpdated(el, binding, vnode);
        });
      } else {
        setSelected(el, binding, vnode.context);
      }
      el._vOptions = [].map.call(el.options, getValue);
    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
      el._vModifiers = binding.modifiers;
      if (!binding.modifiers.lazy) {
        el.addEventListener('compositionstart', onCompositionStart);
        el.addEventListener('compositionend', onCompositionEnd);
        el.addEventListener('change', onCompositionEnd);
        if (isIE9) {
          el.vmodel = true;
        }
      }
    }
  }

为我们的DOM添加compositionstart, compositionend,change三个事件
所以Vue原生DOM的绑定,就是分析我们的模板,生成ast树,上面有我们节点对应的属性方法以及初始值,通过ast生成render函数,通过render函数生成Vnode,最后生成真实的DOM,每个节点会遍历on对象上的事件属性添加对应的监听事件绑定到当前的DOM上。

我们的自定义事件主要就是这四个函数$on $emit $off $once
$on:添加自定义事件
$emit:触发自定义事件
$off:删除自定义事件
$once:添加一个只执行一次的自定义事件
我们来分别看一下源码:

Vue.prototype.$on = function (event, fn) {
    var vm = this;
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn);
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn);
      if (hookRE.test(event)) {
        vm._hasHookEvent = true;
      }
    }
    return vm
  };

$on首先判断是不是数组,如果是就是循环数组,执行$on
其实就是往当前实例vm的_event属性中添加自定义事件名对应的回调函数数组( “自定义事件名”:[fn …])

Vue.prototype.$emit = function (event) {
    var vm = this;
    if (process.env.NODE_ENV !== 'production') {
      var 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 + "\"."
        );
      }
    }
    var cbs = vm._events[event];
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs;
      var args = toArray(arguments, 1);
      var info = "event handler for \"" + event + "\"";
      for (var i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info);
      }
    }
    return vm
  };

$emit首先判断大小写以及回调函数是否存在,如果不满足那么就会报错
如果回调函数存在那么就循环执行

Vue.prototype.$off = function (event, fn) {
    var vm = this;
    // all
    if (!arguments.length) {
      vm._events = Object.create(null);
      return vm
    }
    // array of events
    if (Array.isArray(event)) {
      for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {
        vm.$off(event[i$1], fn);
      }
      return vm
    }
    // specific event
    var cbs = vm._events[event];
    if (!cbs) {
      return vm
    }
    if (!fn) {
      vm._events[event] = null;
      return vm
    }
    // specific handler
    var cb;
    var i = cbs.length;
    while (i--) {
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break
      }
    }
    return vm
  };

$off 如果你什么都不传就会把这个实例上的所有自定义事件都清空
如果你的第一个参数是数组,就会循环自定义数组执行$off
第二个参数没传,就会把这个自定义事件上的所有回调函数清空
如果这个自定义事件不存在,就直接返回当前实例
如果都传了,就会清除掉这个自定义事件对应的引用函数

Vue.prototype.$once = function (event, fn) {
    var vm = this;
    function on () {
      vm.$off(event, on);
      fn.apply(vm, arguments);
    }
    on.fn = fn;
    vm.$on(event, on);
    return vm
  };

$once就是$off和$on的结合,生成一个新的函数,当执行之后就调用$off()将这个自定义事件去除,从而达到了执行一次的效果

以上就是自定义事件的所有源码,就比较清晰,多看几遍就差不多懂了
如果可以还是自己debugger一下,印象会更加深刻

这就是我们今天的分享!!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值