Vue3之事件绑定的实现逻辑详解

 更新时间:2023年11月15日 09:42:47   作者:JonnyLan  
这篇文章主要介绍了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
    )
  }
  
}

如果函数名以update:开头,说明是一个v-model的修改数据函数,这部分逻辑会在v-model专门的文章中介绍;

然后在实例对象的props中找 on+首字母大写的函数名 的函数,如果没找到,则找 on+首字母大写且驼峰命名的函数名 的函数;

如果找到了对应的函数,则调用函数,调用函数的参数为传入的参数。

总结

v-on作用于普通元素底层是利用 addEventListenerremoveEventListener,修饰符要么利用W3C标准,要么利用函数调用来实现;

v-on作用于组件是 子组件利用 emitpro 中搜寻到对应的函数(由父组件传入),然后执行对应的函数。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • vue复杂表格单元格合并根据数据动态合并方式

    vue复杂表格单元格合并根据数据动态合并方式

    这篇文章主要介绍了vue复杂表格单元格合并根据数据动态合并方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-02-02
  • 使用Vue3实现一个简单的思维导图组件

    使用Vue3实现一个简单的思维导图组件

    思维导图是一种用于表示信息、想法和概念的图形化工具,本文将基于 Vue3和VueDraggable实现一个简单的思维导图组件,支持节点拖拽,编辑及节点之间的关系连接,希望对大家有所帮助
    2025-04-04
  • Vue SPA 首屏优化方案

    Vue SPA 首屏优化方案

    这篇文章主要介绍了Vue SPA 首屏优化方案的的相关资料,帮助大家更好的理解和学习使用vue,感兴趣的朋友可以了解下
    2021-02-02
  • vue如何遍历data所有变量并赋值

    vue如何遍历data所有变量并赋值

    这篇文章主要介绍了vue如何遍历data所有变量并赋值,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-04-04
  • Vue3实现虚拟列表的示例代码

    Vue3实现虚拟列表的示例代码

    虚拟列表是一种优化长列表渲染的技术,它可以在保持流畅性的同时,渲染大量的数据,本文主要介绍了如何通过Vue3实现一个虚拟列表,感兴趣的可以了解下
    2024-11-11
  • elementui弹窗页按钮重复提交问题解决方法

    elementui弹窗页按钮重复提交问题解决方法

    本文主要介绍了elementui弹窗页按钮重复提交问题解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-08-08
  • 详解vue-router 路由元信息

    详解vue-router 路由元信息

    本篇文章主要介绍了vue-router 路由元信息,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • Vue-Cli配置代理转发解决跨域问题的方法

    Vue-Cli配置代理转发解决跨域问题的方法

    本文主要介绍了Vue-Cli配置代理转发解决跨域问题的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • vue 集成jTopo 处理方法

    vue 集成jTopo 处理方法

    这篇文章主要介绍了vue 集成jTopo 处理方法,非常不错,具有一定的参考借鉴价值 ,需要的朋友可以参考下
    2019-08-08
  • 解读vant的Uploader上传问题

    解读vant的Uploader上传问题

    这篇文章主要介绍了解读vant的Uploader上传问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-10-10

最新评论