Vue源码解读之事件机制

Vue定义了四种添加事件监听的方法:
clipboard.png
例如$on,可通过vm.$on(event,callback)添加监听。

事件监听

$on

Vue.prototype.$on = function (event, fn) {
    var this$1 = this;
    var vm = this;
    //如果传参event是数组,递归调用$on
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        this$1.$on(event[i], fn);
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn);
      // 这里在注册事件的时候标记bool值也就是个标志位来表明存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。
      if (hookRE.test(event)) {
        vm._hasHookEvent = true;
      }
    }
    return vm
  };

$once

$once监听只能触发一次的事件,触发以后会自动移除该事件。

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

$off

$off用来移除自定义事件。

  Vue.prototype.$off = function (event, fn) {
    var this$1 = this;
    var vm = this;
    // 如果没有参数,关闭全部事件监听器
    if (!arguments.length) {
      vm._events = Object.create(null);
      return vm
    }
    // 关闭数组中的事件监听器
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        this$1.$off(event[i], fn);
      }
      return vm
    }
    // 具体的某个事件
    var cbs = vm._events[event];
    if (!cbs) {
      return vm
    }
    //  fn回调函数不存在,将事件监听器变为null,返回vm
    if (!fn) {
      vm._events[event] = null;
      return vm
    }
    // 回调函数存在
    if (fn) {
      // specific handler
      var cb;
      var i$1 = cbs.length;
      while (i$1--) {
        cb = cbs[i$1];
        if (cb === fn || cb.fn === fn) {
          // 移除 fn 这个事件监听器
          cbs.splice(i$1, 1);
          break
        }
      }
    }
    return vm
  };

$emit

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

  Vue.prototype.$emit = function (event) {
    var vm = this;
    {
      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);
      for (var i = 0, l = cbs.length; i < l; i++) {
        try {
          //触发当前实例上的事件,附加参数都会传给监听器回调。
          cbs[i].apply(vm, args);
        } catch (e) {
          handleError(e, vm, ("event handler for \"" + event + "\""));
        }
      }
    }
    return vm
  };

HTML元素上点击事件

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

<body>
<div id="app">
    <button @click="test01">点我01号</button>
</div>
<script>
    new Vue({
        'el':'#app',
        methods:{
            test01:function(){
                alert('01号被点了!');
            }
        }
    });
</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内有方法test01,因此会执行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;

这里bind用来改变函数中this的指向。(关于call,apply,bind的学习来自于https://www.cnblogs.com/xljzl...
接下来Vue将html解析成ast,解析后的render如图所示

clipboard.png
解析过程参考https://segmentfault.com/a/11...
接下来调用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>

<child-test>标签定义了click事件,若click事件没加修饰符.native,点击按钮不出发任何事件,
若添加了.native修饰符,点击按钮执行的就是test01,alert('01号被点了!');。.native的作用就是在原生dom上绑定事件。
不添加.native的的事件解析过程与上文相同,而添加了.native之后,v-on的事件会被放到nativeOn数组中,解析后的render如图所示:

clipboard.png

在事件初始化,调用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等方法。

事件修饰符及其他

关于事件的使用官网已经说得很清楚啦,这里就不赘述啦。
https://cn.vuejs.org/v2/guide...

<!-- 阻止单击事件继续传播 -->
<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>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值