Vue3的事件绑定的实现逻辑是什么

Vue的事件绑定主要是通过v-on指令来实现的,这个指令既可以实现原生事件绑定,例如onclick等。也可以实现组件的自定义事件,从而实现组件的数据通信。

本文我们就来分析下Vue的事件处理的逻辑。

v-on作用于普通元素

用在普通元素上时,只能监听原生 DOM 事件,最多的就是onclick事件了。我们就以onclick事件来分析原理。

案例
let click = () => {
  console.log("点击我,很快乐")
};

<!-- template -->
<div v-on:click="click">点击我吧</div>
分析实现逻辑
  • 我们先来看下渲染函数
const _hoisted_1 = ["onClick"]

function render(_ctx, _cache) {
  with (_ctx) {
    const { openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

    return (_openBlock(), _createElementBlock("div", { onClick: click }, "点击我吧", 8 /* PROPS */, _hoisted_1))
  }
}

我们看到 渲染函数在创建VNode的时候传了一个onClickpros;

  • 我们先来看下patchProp函数中对onClick这个pros的处理逻辑
export const patchProp: DOMRendererOptions['patchProp'] = (
  el,
  key,
  prevValue,
  nextValue,
  isSVG = false,
  prevChildren,
  parentComponent,
  parentSuspense,
  unmountChildren
) => {
  if (isOn(key)) {
    patchEvent(el, key, prevValue, nextValue, parentComponent)
  }
}

function patchEvent(el, rawName, prevValue, nextValue, instance = null) {
  // vei = vue event invokers
  const invokers = el._vei || (el._vei = {});
  
  if (添加) {
    const invoker = (invokers[rawName] = createInvoker(nextValue, instance));
    el.addEventListener(event, handler, options)
  } else {
    el.removeEventListener(event, handler, options)
  }
}

我们可以看出来底层就是调用的addEventListener函数进行事件监听绑定,调用removeEventListener进行事件监听解绑。其实这个实现逻辑很容易想到,没什么难度。

重点分析—事件修饰符
  • oncecapturepassive

这两个可以直接作为addEventListenerremoveEventListener 的第三个参数options 中的值,因为这是W3C支持的事件可选参数。

  • stop, prevent,capture, self等。

这类修饰符被封装在另外一个withModifiers函数中。

export const withModifiers = (fn: Function, modifiers: string[]) => {
  return (event: Event, ...args: unknown[]) => {
    for (let i = 0; i < modifiers.length; i++) {
      const guard = modifierGuards[modifiers[i]]
      if (guard && guard(event, modifiers)) return
    }
    return fn(event, ...args)
  }
}

这里设计的非常精妙,每个修饰符都对应一个执行函数,如果调用执行函数guard(event, modifiers)返回true, 则函数withModifiers就直接返回了,不会再执行事件的函数fn(event, ...args)了。

这里列一些这些修饰符对应的函数:

const modifierGuards: Record<
  string,
  (e: Event, modifiers: string[]) => void | boolean
> = {
  stop: e => e.stopPropagation(),
  prevent: e => e.preventDefault(),
  self: e => e.target !== e.currentTarget,
  ctrl: e => !(e as KeyedEvent).ctrlKey,
  shift: e => !(e as KeyedEvent).shiftKey,
  alt: e => !(e as KeyedEvent).altKey,
  meta: e => !(e as KeyedEvent).metaKey,
  left: e => 'button' in e && (e as MouseEvent).button !== 0,
  middle: e => 'button' in e && (e as MouseEvent).button !== 1,
  right: e => 'button' in e && (e as MouseEvent).button !== 2,
  exact: (e, modifiers) =>
    systemModifiers.some(m => (e as any)[`${m}Key`] && !modifiers.includes(m))
}

v-on作用于组件绑定自定义事件

实现案例
  • 父组件中 有个子组件son, 使用v-on绑定了子组件的自定义事件,还有一个p显示当前的时间戳。
<Son v-on:children-clicked="childClickedHandler" />
<p>{{ date }}</p>

setup() {

  let childClickedHandler = (data: Date) => {
    date.value = data.getTime();
  }

  let date = ref(new Date().getTime());

  return {
    date,
    childClickedHandler
  };
},
  • 子组件中有一个div, 每次点击会触发自定义事件childrenClicked, 并且传递了一个参数值为当前时间。
<div v-on:click="clickevent">点击我吧</div>

emits: ["childrenClicked"],
setup(props, {emit}) {

  let clickevent = () => {
    emit('childrenClicked', new Date());
  }
  return {clickevent};
},            

这样点击子组件后就会触发父组件的childClickedHandler方法,从而更新当前时间戳的显示。

接下来我们就来看看这底层的逻辑是如何实现的?

实现逻辑
  • 先看下两个组件的渲染函数的重点部分

父组件:

_createVNode(_component_Son, { onChildrenClicked: childClickedHandler }, null, 8 /* PROPS */, ["onChildrenClicked"])

父组件给子组件绑定自定义事件是传递了一个事件pro,这个pro的名称用驼峰命名, 例如本例中的onChildrenClicked

子组件:

const _hoisted_1 = ["onClick"]

_createElementBlock("div", {
    onClick: $event => ($emit('childrenClicked', new Date()))
}, "点击我吧", 8 /* PROPS */, _hoisted_1)

子组件div点击的绑定前面说过,点击的时候执行$emit('childrenClicked', new Date(), 这个没有什么特别的。

现在的问题就是为什么子组件$emit('childrenClicked', new Date()如何找到父组件的onChildrenClicked方法并执行?

  • $emit来自于createSetupContext函数调用时候传入的参数setupContext
export function createSetupContext(
  instance: ComponentInternalInstance
): SetupContext {
    return {
      get attrs() {
        return attrs || (attrs = createAttrsProxy(instance))
      },
      slots: instance.slots,
      emit: instance.emit,
      expose
    }
  }
}

$emit就是组件实例的emit方法。

  • 实例的emit方法用于寻找对应的自定义事件的函数
export function emit(
  instance: ComponentInternalInstance,
  event: string,
  ...rawArgs: any[]
) {
  const props = instance.vnode.props || EMPTY_OBJ

  // 传入的传参
  let args = rawArgs
  
  // TODO: 处理v-mode的方法
  const isModelListener = event.startsWith('update:')

  // 处理函数名,on+首字母大写的函数名 或者 on+驼峰命名的函数名 
  let handlerName
  let handler =
    props[(handlerName = toHandlerKey(event))] ||
    props[(handlerName = toHandlerKey(camelize(event)))]
  if (!handler && isModelListener) {
    handler = props[(handlerName = toHandlerKey(hyphenate(event)))]
  }

  if (handler) {
    // 调用函数,参数是外部传入的参数
    callWithAsyncErrorHandling(
      handler,
      instance,
      ErrorCodes.COMPONENT_EVENT_HANDLER,
      args
    )
  }
  
}
  1. 如果函数名以update:开头,说明是一个v-model的修改数据函数,这部分逻辑会在v-model专门的文章中介绍;
  2. 然后在实例对象的props中找on+首字母大写的函数名的函数,如果没找到,则找on+首字母大写且驼峰命名的函数名的函数;
  3. 如果找到了对应的函数,则调用函数,调用函数的参数为传入的参数。

总结

  1. v-on作用于普通元素底层是利用 addEventListenerremoveEventListener,修饰符要么利用W3C标准,要么利用函数调用来实现;
  2. v-on作用于组件是 子组件利用 emitpro 中搜寻到对应的函数(由父组件传入),然后执行对应的函数。
上一篇:SpringCloudGateway 网关


下一篇:基于vue2快速上手vue3