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中常用的4种高级方法

    VUE中常用的4种高级方法

    provide/inject 是 Vue.js 中用于跨组件传递数据的一种高级技术,它可以将数据注入到一个组件中,然后让它的所有子孙组件都可以访问到这个数据,这篇文章主要介绍了VUE中常用的4种高级方法,需要的朋友可以参考下
    2023-05-05
  • vue+electron实现创建多窗口及窗口间的通信(实施方案)

    vue+electron实现创建多窗口及窗口间的通信(实施方案)

    这篇文章主要介绍了vue+electron实现创建多窗口及窗口间的通信,本文给大家分享实施方案结合实例代码给大家介绍的非常详细,需要的朋友可以参考下
    2022-09-09
  • 解决vue项目axios每次请求session不一致的问题

    解决vue项目axios每次请求session不一致的问题

    这篇文章主要介绍了解决vue项目axios每次请求session不一致的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-10-10
  • Vue新手指南之创建第一个vue-cli脚手架程序

    Vue新手指南之创建第一个vue-cli脚手架程序

    vue-cli 是一个官方发布 vue.js 项目脚手架,使用 vue-cli 可以快速创建 vue 项目。这篇文章主要给大家介绍了关于Vue新手指南之创建第一个vue-cli程序的相关资料,需要的朋友可以参考下
    2021-05-05
  • Vue-Element-Admin前端接入SSO的方法步骤

    Vue-Element-Admin前端接入SSO的方法步骤

    本文主要介绍了Vue-Element-Admin前端接入SSO的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • Vue+Openlayer实现图形的拖动和旋转变形效果

    Vue+Openlayer实现图形的拖动和旋转变形效果

    Openlayer具有自己的扩展插件ol-ext,可以用来实现图形的拖拽、旋转、缩放、拉伸、移动等操作,本文将主要介绍通过Openlayer实现图形的拖动和旋转,需要的同学可以学习一下
    2021-11-11
  • vue3 封装自定义组件v-model的示例

    vue3 封装自定义组件v-model的示例

    这篇文章主要介绍了vue3 封装自定义组件v-model及自定义组件使用v-model,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • vue.js 图片上传并预览及图片更换功能的实现代码

    vue.js 图片上传并预览及图片更换功能的实现代码

    这篇文章主要介绍了vue.js 图片上传并预览及图片更换功能,小编主要围绕我们日常使用功能的例子做讲解,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2018-08-08
  • vue实现用v-bind给src和href赋值

    vue实现用v-bind给src和href赋值

    这篇文章主要介绍了vue实现用v-bind给src和href赋值,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-04-04
  • vue中echarts@4.9版本,地图的使用方式

    vue中echarts@4.9版本,地图的使用方式

    这篇文章主要介绍了vue中echarts@4.9版本地图的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-02-02

最新评论