Vue 3源码深度解析之:`emit`函数:它如何向父组件派发事件。

各位靓仔靓女们,晚上好!我是你们的老朋友,今天咱们来聊聊 Vue 3 里面一个相当重要的函数——emit,也就是事件派发。这玩意儿就像咱古代的烽火台,子组件这边有点啥事儿,赶紧点把火,让父组件那边知道。

咱们这次的讲座,目标就是要把 emit 这家伙扒个精光,看看它到底是怎么把事件从儿子辈儿传到老子辈儿的。

一、emit 是个啥?—— 认识事件派发

在 Vue 的世界里,父子组件之间的数据传递,除了 props 这种老老实实的方式之外,还有一种更灵活的,那就是事件。子组件可以通过 emit 来触发一个自定义事件,然后父组件监听这个事件,就可以拿到子组件传递过来的信息。

你可以把 emit 想象成一个邮递员,子组件把信(事件和数据)交给它,它负责把信送到父组件手里。

二、emit 的基本用法 —— 简单上手

先来个最简单的例子,让你对 emit 有个直观的认识。

// ChildComponent.vue
<template>
  <button @click="handleClick">点我通知父组件</button>
</template>

<script>
import { defineComponent } from 'vue';

export default defineComponent({
  setup(props, { emit }) {
    const handleClick = () => {
      emit('my-event', 'Hello from child!'); // 触发名为 'my-event' 的事件,并传递数据
    };

    return {
      handleClick,
    };
  },
});
</script>
// ParentComponent.vue
<template>
  <ChildComponent @my-event="handleMyEvent" />
  <p>接收到的信息:{{ message }}</p>
</template>

<script>
import { defineComponent, ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default defineComponent({
  components: {
    ChildComponent,
  },
  setup() {
    const message = ref('');

    const handleMyEvent = (data) => {
      message.value = data;
    };

    return {
      message,
      handleMyEvent,
    };
  },
});
</script>

在这个例子里,子组件 ChildComponent 点击按钮时,通过 emit('my-event', 'Hello from child!') 触发了一个名为 my-event 的事件,并传递了一个字符串 'Hello from child!'。父组件 ParentComponent 通过 @my-event="handleMyEvent" 监听了这个事件,并在 handleMyEvent 函数中接收到了子组件传递的数据,然后更新了页面上的 message

三、emit 的参数 —— 灵活传参

emit 函数可以接收多个参数,第一个参数是事件名,后面的参数都是要传递给父组件的数据。

emit('my-event', arg1, arg2, arg3);

父组件在监听事件时,可以接收到这些参数:

<template>
  <ChildComponent @my-event="handleMyEvent" />
</template>

<script>
import { defineComponent } from 'vue';

export default defineComponent({
  methods: {
    handleMyEvent(arg1, arg2, arg3) {
      console.log('arg1:', arg1);
      console.log('arg2:', arg2);
      console.log('arg3:', arg3);
    },
  },
});
</script>

四、emit 的源码解析 —— 深入剖析

接下来,咱们就来扒一扒 emit 的源码,看看它到底是怎么实现的。由于 emit 的实现涉及到 Vue 的整个事件机制,我们不可能把所有代码都贴出来,只能挑一些关键的部分来讲解。

首先,我们需要知道 emit 函数是在哪里定义的。在 Vue 3 中,emit 函数通常是在 setup 函数中通过第二个参数传递进来的。

export default defineComponent({
  setup(props, { emit }) {
    // ...
  },
});

这个 emit 函数实际上是由 Vue 内部的编译器生成的,它会根据组件的配置来决定如何派发事件。

核心逻辑:

emit 的核心逻辑可以简化为以下几个步骤:

  1. 查找事件监听器: 根据事件名,在组件的 propsemits 选项中查找对应的事件监听器。
  2. 执行事件监听器: 如果找到了事件监听器,就依次执行它们,并将 emit 传递的参数传递给这些监听器。

简化版的 emit 实现 (仅用于理解概念):

function emit(instance, event, ...args) {
  // 1. 标准化事件名称
  const eventName = toHandlerKey(camelize(event)); // 例如:my-event -> onMyEvent

  // 2. 获取事件对应的处理函数
  const handlers = instance.props[eventName];

  // 3. 执行处理函数
  if (handlers) {
    if (Array.isArray(handlers)) {
      handlers.forEach(handler => handler(...args));
    } else {
      handlers(...args);
    }
  }
}

function camelize(str) {
  return str.replace(/-(w)/g, (_, c) => c ? c.toUpperCase() : '');
}

function toHandlerKey(str) {
  return str ? 'on' + capitalize(str) : '';
}

function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

源码分析中的关键点:

  • 事件名的标准化: emit 函数会先把事件名转换成标准的事件名,例如,my-event 会被转换成 onMyEvent。这是因为 Vue 的事件监听器通常是以 on 开头的,例如 @my-event 实际上对应的是 onMyEvent 这个 prop
  • 查找事件监听器: emit 函数会在组件的 props 中查找对应的事件监听器。这是因为 Vue 的事件监听器实际上是以 prop 的形式传递给组件的。
  • 执行事件监听器: 如果找到了事件监听器,emit 函数会依次执行它们,并将 emit 传递的参数传递给这些监听器。

五、emits 选项 —— 规范事件派发

在 Vue 3 中,你可以通过 emits 选项来声明组件可以派发的事件。这样做的好处是可以让组件的使用者知道这个组件可以派发哪些事件,并且可以避免一些潜在的错误。

<script>
import { defineComponent } from 'vue';

export default defineComponent({
  emits: ['my-event', 'update:modelValue'], // 声明可以派发的事件
  setup(props, { emit }) {
    const handleClick = () => {
      emit('my-event', 'Hello from child!');
    };

    const updateValue = (newValue) => {
      emit('update:modelValue', newValue);
    }

    return {
      handleClick,
      updateValue,
    };
  },
});
</script>

emits 选项的用法:

  • 声明事件: 你可以在 emits 选项中声明组件可以派发的事件,例如 emits: ['my-event']
  • 验证参数: 你还可以通过 emits 选项来验证 emit 传递的参数。这可以通过一个函数来实现,这个函数接收 emit 传递的参数,并返回一个布尔值,表示参数是否有效。

    <script>
    import { defineComponent } from 'vue';
    
    export default defineComponent({
      emits: {
        'my-event': (data) => {
          if (typeof data === 'string') {
            return true; // 参数有效
          } else {
            console.warn('Invalid data type for my-event: expected string');
            return false; // 参数无效
          }
        },
      },
      setup(props, { emit }) {
        const handleClick = () => {
          emit('my-event', 'Hello from child!');
        };
    
        return {
          handleClick,
        };
      },
    });
    </script>

六、v-modelemit —— 实现双向绑定

v-model 是 Vue 中实现双向绑定的一个语法糖。它实际上是 propsemit 的一个组合。

// ChildComponent.vue
<template>
  <input :value="modelValue" @input="handleInput">
</template>

<script>
import { defineComponent } from 'vue';

export default defineComponent({
  props: {
    modelValue: String,
  },
  emits: ['update:modelValue'], // 声明可以派发的事件
  setup(props, { emit }) {
    const handleInput = (event) => {
      emit('update:modelValue', event.target.value);
    };

    return {
      handleInput,
    };
  },
});
</script>
// ParentComponent.vue
<template>
  <ChildComponent v-model="message" />
  <p>Message: {{ message }}</p>
</template>

<script>
import { defineComponent, ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default defineComponent({
  components: {
    ChildComponent,
  },
  setup() {
    const message = ref('');

    return {
      message,
    };
  },
});
</script>

在这个例子里,ChildComponent 通过 props 接收一个 modelValue,并通过 emit('update:modelValue', newValue) 来更新这个 modelValue。父组件 ParentComponent 使用 v-model="message"messagemodelValue 绑定在一起。当 ChildComponent 中的输入框的值发生变化时,ParentComponent 中的 message 也会随之更新。

v-model 的原理:

v-model 实际上是以下代码的语法糖:

<ChildComponent
  :modelValue="message"
  @update:modelValue="message = $event"
/>

也就是说,v-model 实际上是绑定了一个 modelValue 属性和一个 update:modelValue 事件。当 ChildComponent 触发 update:modelValue 事件时,父组件会更新 message 的值。

七、emit 的最佳实践 —— 提升代码质量

  • 使用 emits 选项: 声明组件可以派发的事件,可以提高代码的可读性和可维护性。
  • 规范事件名: 使用统一的事件名命名规范,例如使用 kebab-case(短横线命名法)。
  • 传递有意义的数据: 尽量传递有意义的数据,避免传递不必要的数据。
  • 避免滥用 emit 在可以使用 props 的情况下,尽量使用 props,避免滥用 emit

八、emit 的常见问题 —— 避坑指南

  • 事件名拼写错误: 检查事件名是否拼写正确,包括大小写和短横线。
  • 事件监听器未定义: 检查父组件是否正确监听了事件。
  • 参数传递错误: 检查 emit 传递的参数是否正确,包括参数的类型和顺序。
  • 循环触发事件: 避免在事件监听器中再次触发相同的事件,这可能会导致无限循环。

九、总结 —— emit 是个好东西

emit 函数是 Vue 中一个非常重要的函数,它可以让子组件向父组件派发事件,实现组件之间的通信。掌握 emit 的用法和原理,可以让你更好地理解 Vue 的组件机制,编写更高效、更易维护的 Vue 代码。

今天就讲到这里,希望大家对 emit 有了更深入的了解。下次有机会,咱们再聊聊 Vue 的其他好玩的东西。 拜拜!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注