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

914 阅读4分钟
Vue 3.0 系列文章

Vue 3.0组件的渲染流程

Vue 3.0组件的更新流程和diff算法详解

揭开Vue3.0 setup函数的神秘面纱

Vue 3.0 Props的初始化和更新流程的细节分析

Vue3.0 响应式实现原理分析

Vue 3.0 计算属性的实现原理分析

Vue3.0 常用响应式API的使用和原理分析(一)

Vue3.0 常用响应式API的使用和原理分析(二)

Vue 3.0 Provide和Inject实现共享数据

Vue 3.0 Teleport的使用和原理分析

Vue3侦听器和异步任务调度, 其中有个神秘角色

Vue3.0 指令

Vue3.0 内置指令的底层细节分析

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

Vue3.0 的双向绑定是如何实现的

Vue3.0的插槽是如何实现的?

探究Vue3.0的keep-alive和动态组件的实现逻辑

Vuex 4.x

Vue Router 4 的使用,一篇文章给你讲透彻

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. 由于在编译阶段在函数名前面加了一个on,例如我们例子的(onChildrenClicked), 所以需要 在实例对象的props中找on+首字母大写的函数名的函数,如果没找到,则找on+首字母大写且驼峰命名的函数名的函数;
  3. 如果找到了对应的函数,则调用函数,调用函数的参数为传入的参数。

总结

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