Vue之事件相关

前言

本篇文章带来Vue.js的事件机制,具体的分析点如下:

  • $emit、$on、$off、$once背后的处理逻辑
  • @click形式背后的处理逻辑

具体逻辑梳理

实际上在 Vue初始化 这篇文章中就提及了事件相关实例方法的创建,这里就在具体说下。

在Vue.js文件加载执行,其中会执行eventsMixin函数,该函数的作用就是:

创建事件相关的的原型方法,即$on、$once、$off、$emit

主要源码如下:

function eventsMixin (Vue) {
    var hookRE = /^hook:/;
    Vue.prototype.$on = function(event, fn) { // codes };
    Vue.prototype.$once = function(event, fn) { // codes };
    Vue.prototype.$off = function(event, fn) { // codes };
    Vue.prototype.$emit = function(event) { // codes };
}

下面就来具体看看每个事件方法背后的处理逻辑(每个方法的处理逻辑不是很复杂,这里会把源码贴出来)。

$emit

该方法用于事件的触发操作

Vue.prototype.$emit = function (event) {
    var vm = this;
    {
      var lowerCaseEvent = event.toLowerCase();
      // 如果事件名是大写并且该事件存在
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        // 输出提示
      }
    }
    // 支持事件对应多个处理函数
    var cbs = vm._events[event];
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs;
      // $emit支持传参
      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
  };

从上面的源码中可以知道三件事:

  • 非小写事件名会有提示
  • $emit支持传递多个参数
  • 事件的注册中心就是Vue实例的_events变量,该变量保存中当前Vue实例的所有事件

疑问1: _events是如何收集事件的以及在哪里收集的呢?

$on

注册事件

  Vue.prototype.$on = function (event, fn) {
    var this$1 = this;

    var vm = this;
    // 支持批量注册事件
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        this$1.$on(event[i], fn);
      }
    } else {
      // 注册事件到_events中,并且知道_events是个对象
      (vm._events[event] || (vm._events[event] = [])).push(fn);
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      // 判断是否是hook:开头的事件,这类事件会触发hook:开头的生命周期事件
      if (hookRE.test(event)) {
        vm._hasHookEvent = true;
      }
    }
    return vm
  };

通过$on方法的逻辑可以知道:

  • 事件注册是通过 o n on来实现的(解答了上面 emit的疑问)
  • events是一个对象,实际上再准确点,vm._events是在init方法中initEvents函数中定义的

    vm._events = Object.create(null);
    vm._hasHookEvent = false;

  • 针对hook:开头的事件,实际上在callHook中调用

这里展开下callHook方法的,该方法主要执行生命周期函数,例如beforeCreate、created、mounted等。
callHook中涉及到hook:事件

  if (vm._hasHookEvent) {
    /*
    	请注意这里hook就是生命周期函数的名称,$emit会触发它们
    	hook:beforeCreate
    	hook:created
    	...
    */
    vm.$emit('hook:' + hook);
  }

疑问2:hook:开头的生命周期对应的事件是用来做什么的

如果这里使用者自定义事件名与Vue的生命周期同名,就有意思了,例如:

created() {
    this.$on('hook:created', () => {})
}

那实际上按照上面的逻辑,因为callHook中$emit触发这里会自动执行。

$once和$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
  };

$off方法的处理就不贴代码了,主要就是调用数组的splice来删除保存的事件处理函数

@click或v-on:click的相关处理

在Vue项目中使用浏览器事件,简单示例:

<button @click="handleClick">
    点击
</button>

这里较为详细的解析可参考之前的文章,这里就简要提及下,Vue将template中内容解析构成render函数,即:

with(this){
    return _c('div',{
        attrs:{"id":"app"}
    },[
        _c('button',{
            on:{"click":handleClick}}
          )
    ])
}

实际上就是Vue官方JSX那边的格式,详情可点击查看 。而这里主要关注点在于vnode -> html这部分解析中,事件相关的注册以及调用处理,这里也暂时只关注事件处理相关(patch相关的后续会专门详细梳理)。
实际上通过源码逻辑的梳理,针对事件相关的处理的入口方法是:

updateDOMListeners

而该方法中主要逻辑如下:

function updateDOMListeners (oldVnode, vnode) {
  // 新旧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;
}

从上面可知,内部实际上是调用了updateListeners方法,具体看看updateListeners的处理逻辑,流程逻辑如下:
在这里插入图片描述
从上面的逻辑中需要关注两个步骤的处理逻辑,这里就具体暂开:

  • 相关处理步骤
  • add$1函数的具体处理逻辑
updateListener中的相关处理
if (isUndef(cur)) {
    // 报错提示
} else if (isUndef(old)) {
    // 旧vnode没有事件
    // 新vnode事件处理函数没有fns属性
	if (isUndef(cur.fns)) {
    	cur = on[name] = createFnInvoker(cur);
    }
    // 调用add(即add$1)函数
    add(event.name, cur, event.once, event.capture, event.passive, event.params);
} else if (cur !== old) {
	old.fns = cur;
    on[name] = old;
}
createFnInvoker函数
function createFnInvoker (fns) {
  function invoker () {
    var arguments$1 = arguments;

    var fns = invoker.fns;
    // 可知vue中html中函数定义支持多个函数公共处理
    if (Array.isArray(fns)) {
      var cloned = fns.slice();
      for (var i = 0; i < cloned.length; i++) {
        cloned[i].apply(null, arguments$1);
      }
    } else {
      // return handler return value for single handlers
      return fns.apply(null, arguments)
    }
  }
  // 定义fns属性,实际上fns就是事件处理函数
  invoker.fns = fns;
  return invoker
}
add$1函数

核心函数,实现函数的注册

function add$1 (
  event,
  handler,
  once$$1,
  capture,
  passive
) {
  handler = withMacroTask(handler);
  if (once$$1) { handler = createOnceHandler(handler, event, capture); }
  target$1.addEventListener(
    event,
    handler,
    supportsPassive
      ? { capture: capture, passive: passive }
      : capture
  );
}

从add$1函数的逻辑中,很清晰的知道下面三点信息:

  • 使用addEventListener来实现事件绑定
  • withMacroTask函数处理了事件处理处理
  • 事件只响应一次调用createOnceHandler函数做了特殊处理
function withMacroTask (fn) {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true;
    var res = fn.apply(null, arguments);
    useMacroTask = false;
    return res
  })
}

从上面源码可知,就是定义了_withTask方法,而该方法中在事件处理之前设置了全局useMacroTask为true,

而该全局属性只会在 n e x t T i c k 使 nextTick函数中使用到,这里你可以查看之前的 nextTick相关的文章。这里估计是处理事件和$nextTick之间调用时间导致新旧vnode导致的问题。

createOnceHandler函数
function createOnceHandler (handler, event, capture) {
  var _target = target$1; // save current target element in closure
  return function onceHandler () {
    var res = handler.apply(null, arguments);
    if (res !== null) {
      // 从上面add$1可知,该函数必然调用了removeEventListener
      remove$2(event, onceHandler, capture, _target);
    }
  }
}

总结

Vue中事件相关:

  • _events中保存的当前实例对象的所有的事件定义
  • 对于事件支持多个事件处理函数,也支持一次响应
  • hook:开头的对应的生命周期名称事件会被自动执行,例如:hook:created
  • @click绑定事件,Vue底层是使用addEventListener来实现事件绑定的

猜你喜欢

转载自blog.csdn.net/s1879046/article/details/86654511