DOM 事件与自定义事件如何用 DOM 事件与自定义事件从编译到生成事件对象的阶段 编译 addHandler 生成代码数据 DOM 事件执行时做了什么 自定义事件都做了什么 父子组件通信的实质 子组件通过父组件传入 listener,去通过$on方式去添加,当$emit 的时候会去执行这个添加的函数。 $emit是往自身派发事件,$emit 是往 vue 实例派发事件,子组件调用$emit 方法…

DOM 事件与自定义事件如何用

let child = {
  template: `<div @click="handler($event)">div</div>`,
  methods: {
    handler(e) {
      console.log(e);
      this.$emit("select");
    }
  }
};
var father = {
  el: "#app",
  template: `<div @select="select"><Child @select="select" @click.native.prevent="handler"></Child></div>`,
  methods: {
    handler() {
      console.log("Child clicked!");
    },
    select() {
      console.log("Child select!");
    }
  },
  components: {
    Child
  }
};

DOM 事件与自定义事件从编译到生成事件对象的阶段

    1. 编译
// src/compiler/parser/index.js
export const onRE = /^@|^v-on:/;
export const dirRE = /^v-|^@|^:/;
export const bindRE = /^:|^v-bind:/;

function parseModifiers(name: string): Object | void {
  const match = name.match(modifierRE);
  if (match) {
    const ret = {};
    match.forEach((m) => {
      ret[m.slice(1)] = true;
    });
    return ret;
  }
}
function processAttrs(el) {
  const list = el.attrsList;
  let i, l, name, rawName, value, modifiers, isProp;
  for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name;
    value = list[i].value;
    // 对标签属性进行处理,如果是指令(v-),先解析出修饰符
    if (dirRE.test(name)) {
      // mark element as dynamic
      el.hasBindings = true;
      // 把修饰符转换成对象
      modifiers = parseModifiers(name);
      if (modifiers) {
        name = name.replace(modifierRE, "");
      }
      if (bindRE.test(name)) {
        // v-bind
      } else if (onRE.test(name)) {
        // 如果是事件指令
        name = name.replace(onRE, "");
        addHandler(el, name, value, modifiers, false, warn);
      } else {
        //
      }
    } else {
      //
    }
  }
}
    1. addHandler
// src/compiler/helpers.js
// 做了三件事:1、处理修饰符对事件名处理,2、根据修饰符判断是原生事件,还是普通事件,3、缓存回调函数,调用时候用
export function addHandler(el: ASTElement, name: string, value: string, modifiers: ?ASTModifiers, important?: boolean, warn?: Function) {
  modifiers = modifiers || emptyObject;
  // warn prevent and passive modifier
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== "production" && warn && modifiers.prevent && modifiers.passive) {
    warn("passive and prevent can't be used together. " + "Passive handler can't prevent default event.");
  }
  // 1、处理修饰符对事件名处理
  if (modifiers.capture) {
    delete modifiers.capture;
    name = "!" + name; // mark the event as captured
  }
  if (modifiers.once) {
    delete modifiers.once;
    name = "~" + name; // mark the event as once
  }
  /* istanbul ignore if */
  if (modifiers.passive) {
    delete modifiers.passive;
    name = "&" + name; // mark the event as passive
  }

  // 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 (name === "click") {
    if (modifiers.right) {
      name = "contextmenu";
      delete modifiers.right;
    } else if (modifiers.middle) {
      name = "mouseup";
    }
  }

  // 2、根据修饰符判断是原生事件,还是普通事件
  let events;
  if (modifiers.native) {
    delete modifiers.native;
    events = el.nativeEvents || (el.nativeEvents = {});
  } else {
    events = el.events || (el.events = {});
  }

  const newHandler: any = {
    value: value.trim()
  };
  if (modifiers !== emptyObject) {
    newHandler.modifiers = modifiers;
  }

  // 3、缓存回调函数,调用时候用
  const 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 = {
  click: {
    value: "click($event)"
  }
};
// 父组件
el.events = {
  select: {
    value: "select"
  }
};
el.nativeEvents = {
  click: {
    value: "handler",
    modifiers: {
      prevent: true
    }
  }
};
    1. 生成代码数据
// src/compiler/codegen/index.js
// 根据events和nativeEvents生成data数据
export function genData(el: ASTElement, state: CodegenState): string {
  let data = "{";
  // ...
  if (el.events) {
    data += `${genHandlers(el.events, false, state.warn)},`;
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true, state.warn)},`;
  }
  // ...
  return data;
}
// src/compiler/codegen/events.js
// 修饰符不存在,如果handler.value是函数调用路径或者函数函数表达式,return handler.value
// 修饰符存在,根据handler.value,处理一下修饰符,最终返回一个函数表达式
export function genHandlers(events: ASTElementHandlers, isNative: boolean, warn: Function): string {
  let res = isNative ? "nativeOn:{" : "on:{";
  for (const name in events) {
    res += `"${name}":${genHandler(name, events[name])},`;
  }
  return res.slice(0, -1) + "}";
}

const fnExpRE = /^\s*([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/;
const simplePathRE = /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?']|\[".*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*\s*$/;
function genHandler(name: string, handler: ASTElementHandler | Array<ASTElementHandler>): string {
  if (!handler) {
    return "function(){}";
  }

  if (Array.isArray(handler)) {
    return `[${handler.map((handler) => genHandler(name, handler)).join(",")}]`;
  }

  const isMethodPath = simplePathRE.test(handler.value);
  const isFunctionExpression = fnExpRE.test(handler.value);

  if (!handler.modifiers) {
    if (isMethodPath || isFunctionExpression) {
      return handler.value;
    }
    return `function($event){${handler.value}}`; // inline statement
  } else {
    let code = "";
    let genModifierCode = "";
    const keys = [];
    for (const key in handler.modifiers) {
      if (modifierCode[key]) {
        genModifierCode += modifierCode[key];
        // left/right
        if (keyCodes[key]) {
          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);
    }
    // Make sure modifiers like prevent and stop get executed after key filtering
    if (genModifierCode) {
      code += genModifierCode;
    }
    const handlerCode = isMethodPath ? `return ${handler.value}($event)` : isFunctionExpression ? `return (${handler.value})($event)` : handler.value;

    return `function($event){${code}${handlerCode}}`;
  }
}
// 最终生成
// 父组件
{
  on: {"select": select},
  nativeOn: {"click": function($event) {
      $event.preventDefault();
      return handler($event)
    }
  }
}
// 子组件
{
  on: {"click": function($event) {
      handler($event)
    }
  }
}

DOM 事件执行时做了什么

// src/platforms/web/runtime/modules/event.js
// patch的更新与创建会执行updateDOMListeners
let target: any;
function updateDOMListeners(oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return;
  }
  // 获取事件对象
  const on = vnode.data.on || {};
  const oldOn = oldVnode.data.on || {};
  // 缓存vnode的dom对象
  target = vnode.elm;
  // 遍历on加事件监听,遍历oldOn移除事件监听,事件可以是原生dom事件,也可以是自定义事件
  updateListeners(on, oldOn, add, remove, vnode.context);
  target = undefined;
}

// src/core/vdom/helpers/update-listeners.js
export function updateListeners(on: Object, oldOn: Object, add: Function, remove: Function, vm: Component) {
  let name, def, cur, old, event;
  // 遍历on加事件监听
  for (name in on) {
    def = cur = on[name];
    old = oldOn[name];
    // 判断事件是否有once、capture、passive 等修饰符
    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);
      }
      // 完成事件绑定,用addeventListener添加事件,如果在添加dom事件回调函数之间执行,会把更改的推入队列,当作macroTask任务在nextTick执行
      add(event.name, cur, event.once, event.capture, event.passive, event.params);
    } else if (cur !== old) {
      old.fns = cur;
      on[name] = old;
    }
  }
  // 遍历oldOn移除事件监听
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name);
      // 移除事件绑定,用removeEventListener移除事件,如果在移除dom事件回调函数之间执行,会把更改的推入队列,当作macroTask任务在nextTick执行
      remove(event.name, oldOn[name], event.capture);
    }
  }
}
// 判断事件是否有once、capture、passive 等修饰符
// src/core/vdom/helpers/update-listeners.js
const normalizeEvent = cached(
  (
    name: string
  ): {
    name: string,
    once: boolean,
    capture: boolean,
    passive: boolean,
    handler?: Function,
    params?: Array<any>
  } => {
    const passive = name.charAt(0) === "&";
    name = passive ? name.slice(1) : name;
    const once = name.charAt(0) === "~"; // Prefixed last, checked first
    name = once ? name.slice(1) : name;
    const capture = name.charAt(0) === "!";
    name = capture ? name.slice(1) : name;
    return {
      name,
      once,
      capture,
      passive
    };
  }
);
// 通过保留引用关系,保证事件回调只添加一次,以后修改只需要修改回调函数的引用
// src/core/vdom/helpers/update-listeners.js
export function createFnInvoker(fns: Function | Array<Function>): Function {
  function invoker() {
    const fns = invoker.fns;
    if (Array.isArray(fns)) {
      const cloned = fns.slice();
      for (let i = 0; i < cloned.length; i++) {
        cloned[i].apply(null, arguments);
      }
    } else {
      return fns.apply(null, arguments);
    }
  }
  invoker.fns = fns;
  return invoker;
}

自定义事件都做了什么

// src/core/vdom/create-component.js
// 首先创建一个vnode
export function createComponent(Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string): VNode | Array<VNode> | void {
  // ...
  const listeners = data.on;
  // 获取自定义事件
  data.on = data.nativeOn;

  const name = Ctor.options.name || tag;
  // 把listeners,作为生成vnode的一个参数传入,处理环境是子组件,
  const vnode = new VNode(`vue-component-${Ctor.cid}${name ? `-${name}` : ""}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory);

  return vnode;
}
// src/core/instance/init.js
// 先初始化下父组件传入的listener
export function initInternalComponent(vm: Component, options: InternalComponentOptions) {
  const opts = (vm.$options = Object.create(vm.constructor.options));
  // ....
  const vnodeComponentOptions = parentVnode.componentOptions;

  opts._parentListeners = vnodeComponentOptions.listeners;
  // ...
}
// src/core/instance/events.js
// 带一些参数中转下listeners
export function initEvents(vm: Component) {
  vm._events = Object.create(null);
  vm._hasHookEvent = false;
  // init parent attached events
  const listeners = vm.$options._parentListeners;
  if (listeners) {
    updateComponentListeners(vm, listeners);
  }
}
// 然后对自定义事件进行add和remove
// src/core/instance/events.js
let target: any;
export function updateComponentListeners(vm: Component, listeners: Object, oldListeners: ?Object) {
  target = vm;
  updateListeners(listeners, oldListeners || {}, add, remove, vm);
  target = undefined;
}
// 自定义事件的add和remove,是通过vue的事件中心实现的
// src/core/instance/events.js
function add(event, fn, once) {
  if (once) {
    target.$once(event, fn);
  } else {
    target.$on(event, fn);
  }
}

function remove(event, fn) {
  target.$off(event, fn);
}
// 自定义事件的监听与移除监听用 vue 的事件中心实现的
// 1. 执行$on 先把事件回调函数按照事件名存起来
// 2. 执行$emit 根据事件名,找到回调函数,遍历执行回调函数
// 3. 执行$off 移除事件名和事件名对应的回调函数
// 4. 执行$once, 执行$on后再执行$off
export function eventsMixin(Vue: Class<Component>) {
  const hookRE = /^hook:/;
  Vue.prototype.$on = function(event: string | Array<string>, fn: Function): Component {
    const vm: Component = this;
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$on(event[i], fn);
      }
    } else {
      // 1. 执行$on 先把事件按照事件名称存起来
      (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
      if (hookRE.test(event)) {
        vm._hasHookEvent = true;
      }
    }
    return vm;
  };

  // 4. 执行$once, 执行$on后再执行$off
  Vue.prototype.$once = function(event: string, fn: Function): Component {
    const vm: Component = this;
    function on() {
      vm.$off(event, on);
      fn.apply(vm, arguments);
    }
    on.fn = fn;
    vm.$on(event, on);
    return vm;
  };

  // 3. 执行$off 移除事件名和事件名对应的回调函数
  Vue.prototype.$off = function(event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this;
    // all
    if (!arguments.length) {
      vm._events = Object.create(null);
      return vm;
    }
    // array of events
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$off(event[i], fn);
      }
      return vm;
    }
    // specific event
    const cbs = vm._events[event];
    if (!cbs) {
      return vm;
    }
    if (!fn) {
      vm._events[event] = null;
      return vm;
    }
    if (fn) {
      // specific handler
      let cb;
      let i = cbs.length;
      while (i--) {
        cb = cbs[i];
        if (cb === fn || cb.fn === fn) {
          cbs.splice(i, 1);
          break;
        }
      }
    }
    return vm;
  };

  // 2. 执行$emit 根据事件名,找到回调函数,遍历执行回调函数
  Vue.prototype.$emit = function(event: string): Component {
    const vm: Component = this;
    if (process.env.NODE_ENV !== "production") {
      const 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}".`);
      }
    }
    let cbs = vm._events[event];
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs;
      const args = toArray(arguments, 1);
      for (let 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;
  };
}

父子组件通信的实质

  1. 子组件通过父组件传入 listener,去通过$on方式去添加,当$emit 的时候会去执行这个添加的函数。
  2. $emit是往自身派发事件,$emit 是往 vue 实例派发事件,子组件调用$emit 方法的时候,往当前子组件派发事件,因为找的是当前子组件的 events,为什么会有父子组件通信,是因为回调函数是定义的父组件中,实际上却是在子组件实例上监听这个自定义事件,父组件的 listener 会在子组件创建中传入,在子组件初始化中,拿到父组件传入的事件回调,事件回调又定义在父组件的环境中,当子组件自身派发这个事件的时候,回调就执行了,回调是父环境,所以就达成了父子组件通信

暂无评论