# Vue Event 的浅析

# 编译阶段

# HTML 解析阶段

我们还是从 vue 的编译阶段开始分析 vue 事件的处理方式

之前分析过,在html-parse阶段处理标签的闭合阶段会调用processElement方法来处理标签元素上的绑定,该方法内部会调用processAttrs来处理事件等相关绑定。代码在src/compiler/parse/index.js

function processAttrs(el) {
  const list = el.attrsList;
  let i, l, name, rawName, value, modifiers, syncGen, isDynamic;
  for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name;
    value = list[i].value;
    if (dirRE.test(name)) {
      el.hasBindings = true;
      modifiers = parseModifiers(name.replace(dirRE, '')); //绑定的修饰符 exp:{ stop:true,native:true }
      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, list[i], isDynamic);
      }
    }
  }
}

这里可以看到,处理事件的核心是是调用了addHandler方法,同时传入事件的修饰符等属性。接着看下addHandler的实现,该方法位于src/compiler/helpers.js

function addHandler(
  el: ASTElement,
  name: string,
  value: string,
  modifiers: ?ASTModifiers,
  important?: boolean,
  warn?: ?Function,
  range?: Range,
  dynamic?: boolean,
) {
  modifiers = modifiers || emptyObject;
  if (modifiers.right) {
    //处理right修饰符事件
  } else if (modifiers.middle) {
    //处理middle修饰符事件
  }
  if (modifiers.capture) {
    delete modifiers.capture;
    name = prependModifierMarker('!', name, dynamic);
  }
  if (modifiers.once) {
    delete modifiers.once;
    name = prependModifierMarker('~', name, dynamic);
  }
  if (modifiers.passive) {
    delete modifiers.passive;
    name = prependModifierMarker('&', name, dynamic);
    //prependModifierMarker 方法,根据事件名是否是动态的 做一些修饰符的处理
  }
  let events;
  if (modifiers.native) {
    delete modifiers.native;
    events = el.nativeEvents || (el.nativeEvents = {});
  } else {
    events = el.events || (el.events = {});
  }
  const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range); //设置属性节点的start end
  if (modifiers !== emptyObject) {
    newHandler.modifiers = modifiers;
  }

  const handlers = events[name];
  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;
}

可以看到,首先是对事件的部分修饰符做了一些前置处理,这些处理主要是修改事件的name属性。然后根据是否有native修饰符,将事件添加到了改 ast 节点的nativeEvents或者events属性中。然后再根据事件是否存在或是否是important事件,完成对 ast 事件属性的修改。

# codegen 阶段

接着我们看在render函数生成阶段对事件的处理

根据我们之前的了解,codegen中有一个十分重要的方法调用genData。位于src/compiler/codegen/index.js

function genData(el, state) {
  if (el.events) {
    data += `${genHandlers(el.events, false)},`;
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true)},`;
  }
}

可以看到这里是先后处理了 ast 节点上的events nativeEvents2 个属性。接着我们看下genHandlers方法的实现

function genHandlers(events: ASTElementHandlers, isNative: boolean) {
  const prefix = isNative ? 'nativeOn:' : 'on:';
  let staticHandlers = ``;
  let dynamicHandlers = ``;
  for (const name in events) {
    const handlerCode = genHandler(events[name]);
    if (events[name] && events[name].dynamic) {
      dynamicHandlers += `${name},${handlerCode},`; //数组格式
    } else {
      staticHandlers += `"${name}":${handlerCode},`; //对象格式
    }
  }
  staticHandlers = `{${staticHandlers.slice(0, -1)}}`;
  if (dynamicHandlers) {
    return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])`;
  } else {
    return prefix + staticHandlers;
  }
}

可以看到,该方法内部是调用了genHandler来返回单个事件的函数字符串。然后根据是否拥有dynamic函数,决定返回的字符串格式,如果有dynamic函数则直接返回一个函数_d函数字符串,静态函数是第一个参数,动态的函数放在一个数组里面作为第二个参数。关于_d函数,在src/core/instance/bind-dynamic-keys.js下。代码很简单,就是将第二个参数的奇数位作为key,偶数位作为value,循环赋值到第一个参数里面去。可以自己去看一下

接下来我们看下核心的genHandler方法的实现:

function genHandler() {
  if (!handler) {
    return 'function(){}';
  }
  if (Array.isArray(handler)) {
    return `[${handler.map((handler) => genHandler(handler)).join(',')}]`;
  }
  const isMethodPath = simplePathRE.test(handler.value); //匹配是否是一个函数名
  const isFunctionExpression = fnExpRE.test(handler.value); //匹配是否是一个函数
  const isFunctionInvocation = simplePathRE.test(
    handler.value.replace(fnInvokeRE, ''),
  ); //匹配是否是一个函数体

  if (!handler.modifiers) {
    if (isMethodPath || isFunctionExpression) {
      return handler.value;
    }
    return `function($event){${
      isFunctionInvocation ? `return ${handler.value}` : handler.value
    }}`;
  } else {
    let code = '';
    let genModifierCode = ''; // 修饰符代码
    const keys = [];
    for (const key in handler.modifiers) {
      if (modifierCode[key]) {
        genModifierCode += modifierCode[key]; //获取修饰符对应的实现代码
        if (keyCodes[key]) {
          //键盘事件code
          keys.push(key);
        }
      } else if (key === 'exact') {
        //组合键处理
        const modifiers: ASTModifiers = (handler.modifiers: any);
        genModifierCode += genGuard(
          ['ctrl', 'shift', 'alt', 'meta']
            .filter((keyModifier) => !modifiers[keyModifier])
            .map((keyModifier) => `$event.${keyModifier}Key`)
            .join('||'),
        );
      } else {
        keys.push(key);
      }
    }
    if (keys.length) {
      code += genKeyFilter(keys); //处理键盘的事件
    }
    const handlerCode = isMethodPath
      ? `return ${handler.value}.apply(null, arguments)`
      : isFunctionExpression
      ? `return (${handler.value}).apply(null, arguments)`
      : isFunctionInvocation
      ? `return ${handler.value}`
      : handler.value;
    return `function($event){${code}${handlerCode}}`;
  }
}

这里,首先对多个同名事件绑定的情况做了个递归的调用。接着用正则判断了当前事件函数的类型。如果当前事件没有修饰符的话,则根据函数的类型,决定直接返回函数还是包裹一层function($event){}进行返回.

接着处理事件有修饰符的情况。当事件含有修饰符的情况下,首先处理 vue 内置的修饰符,获取到修饰符对应的实现代码字符串进行拼接。这部分代码部分如下:

const modifierCode = {
  stop: '$event.stopPropagation();',
  prevent: '$event.preventDefault();',
  self: genGuard(`$event.target !== $event.currentTarget`),
  // ....
};

可以看到所谓的修饰符其实就是段实现代码的索引

接着判断修饰符中是否有组合键修饰符,过滤ctrl shift alt meta4 种组合键的情况。接着通过genKeyFilter方法处理其他的修饰符主要一些按键的修饰符。具体不多做阐述。

接着判断了函数的类型,生成不同的执行 code。最后将修饰符的 code 与函数的 code 通过一层function函数包裹返回。这样事件在编译阶段的全部工作就结束了。

# 其他

其实在codegen阶段,除了上面提到的events nativeEvents外还有一个地方涉及到了事件的处理

function genData(el, state) {
  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);
  }
}

这里的wrapListeners其实是v-on指令在没有指定 event name 情况下会存在的属性。大家可以看下指令的处理方法和src/compiler/directives/on.js方法。这个wrapListeners最终会在生成的data对象外层包裹一个_g()的方法调用,具体功能就是在vnode生成阶段把v-on指定的对象动态的挂载到vnodeon属性中。接着我们看下在组件挂载阶段的事件处理

# 挂载阶段

# 原始标签元素的事件绑定

先简单的看下 vnode 的钩子函数,在高阶函数createPatchFunction方法种,有这样的一段代码

const hooks = ['create', 'activate', 'update', 'remove', 'destroy'];

function createPatchFunction(backend) {
  let i, j;
  const cbs = {};
  const { modules, nodeOps } = backend;

  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = [];
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]]);
      }
    }
  }
  return function patch() {
    //...
  };
}

可以看到首先是根据传入的参数,初始化了一个cbs的对象,这个就是 vnode 的钩子函数,钩子函数会在 vnode 的不同阶段调用。event相关的钩子函数在src/platforms/web/runtime/modules/events.js下面。dom 在创建完成插入到文档前会执行create钩子,对应到event就是updateDOMListeners方法,这个方法核心就是调用了src/core/vdom/helpers/update-listeners.js方法

function updateListeners(
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component,
) {
  let name, def, cur, old, event;
  for (name in on) {
    def = cur = on[name];
    old = oldOn[name];
    event = normalizeEvent(name); // 对 once passive capture 这几个修饰符做处理
    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(event.name, oldOn[name], event.capture);
    }
  }
}

function createFnInvoker(fns, vm) {
  function invoker() {
    const fns = invoker.fns;
    if (Array.isArray(fns)) {
      const cloned = fns.slice();
      for (let i = 0; i < cloned.length; i++) {
        invokeWithErrorHandling(cloned[i], null, arguments, vm, `v-on handler`);
      }
    } else {
      // return handler return value for single handlers
      return invokeWithErrorHandling(fns, null, arguments, vm, `v-on handler`);
    }
  }
  invoker.fns = fns;
  return invoker;
}

可以看到首先循环newVnode上的事件,判断在oldVnode上是否存在。不存在的话则调用createFnInvoker方法创建一个方法。注意createFnInvoker返回一个函数,并将我们的原始函数作为它的一个属性fns,在返回的函数内部调用我们原始的函数。接着调用add方法进行事件绑定,这个add就是原始的addEventListener包装。如果oldVnode上存在这个事件绑定,则直接替换掉事件函数的fns属性即可,减少了一次绑定调用

接着循环oldVnode上的事件,如果newVnode上没有定义,则直接移除即可。

# Component 组件的事件绑定

首先在组件的 vnode 生成阶段,会调用src/core/vdom/create-component.js内的createComponent方法

function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string,
) {
  //...
  const listeners = data.on;
  data.on = data.nativeOn;
  //...

  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory,
  );

  return vnode;
}

可以看到,首先将组件的事件on赋值给了一个listeners变量,接着将data.nativeOn赋值给了data.on属性。然后将listeners作为一个参数传递给了VNode构造函数。从这里就将 component 组件的组件事件与 dom 事件进行了区分处理,dom 事件上面已经说过了,我们主要看组件事件listeners的处理。

组件的 vnode 构建完成后,接着就到了组件的挂载阶段,会执行到组件的_init方法,从而进入到initInternalComponent方法。

function initInternalComponent() {
  const opts = (vm.$options = Object.create(vm.constructor.options));
  const parentVnode = options._parentVnode;
  const vnodeComponentOptions = parentVnode.componentOptions;
  opts._parentListeners = vnodeComponentOptions.listeners;
  //...
}

从这里可以得知,子组件的_parentListeners会存储父组件的listeners属性,接着调用initEvent方法,定义在src/core/instance/events.js中。

function initEvents(vm) {
  vm._events = Object.create(null);
  vm._hasHookEvent = false;
  const listeners = vm.$options._parentListeners; //监听父组件的事件对象
  if (listeners) {
    updateComponentListeners(vm, listeners);
  }
}

updateComponentListeners核心也是调用updateListeners方法,上面已经说过了。主要的区别是这次add remove这些方法是调用了组件的专用事件处理方法。具体见eventsMixin这个方法。核心就是个发布订阅模式,子组件通过$emit来调用$on注册的事件。

# 总结

  • html解析阶段会根据事件的修饰符将事件存入ast的eventsnativeEvents属性中
  • codegen 阶段会根据组件的修复符,生成对应的函数字符串,存储在渲染函数data的onnativeOn属性
  • vonde的生成阶段,如果是组件的话则将nativeOn赋值给on,将原先的on作为参数传递给组件的VNode构造函数
  • DOM元素挂载阶段,DOM元素创建后,执行vnode的create钩子函数,调用原生的事件方法,将事件挂载到dom元素上
  • 组件挂载阶段,在组件的初始化阶段中,首先获取到父vnode元素的listeners属性,然后在initEvent阶段建立自定义的事件绑定