探讨 Vue 3 源码中如何处理组件的事件注册 (`@click`) 和事件派发 (`emit`)。

Vue 3 事件机制:一场来自远方的“投石问路”

各位观众,晚上好!我是你们的老朋友,今天咱们聊聊 Vue 3 里那些“你点我也点”的事件处理机制。 想象一下,组件就像一个个小城堡,有的负责展示信息,有的负责处理用户交互。而事件,就像是城堡间的“投石问路”,一个城堡想通知另一个城堡发生了啥事儿,就得通过事件这个“石头”扔过去。

那么,Vue 3 是如何巧妙地实现这个“投石问路”的过程的呢?咱们慢慢展开。

一、事件注册:给城堡安上“监听器”

在 Vue 组件中,我们经常用 @click@input 这样的语法来监听事件。这背后到底发生了什么? 其实,这就是在给组件的 DOM 元素或者组件本身安装“监听器”。

1. 模板编译阶段:提取事件信息

当 Vue 编译模板时,会扫描所有的 HTML 标签和组件标签,找到带有 @ 符号的属性。 比如:

<template>
  <button @click="handleClick">点我</button>
  <MyComponent @custom-event="handleCustomEvent"></MyComponent>
</template>

Vue 的编译器会识别出 @click@custom-event,并将它们的信息提取出来,生成对应的渲染函数 (render function)。这个渲染函数会包含如何将事件监听器绑定到 DOM 元素或子组件上的指令。

2. 渲染函数:绑定事件监听器

渲染函数最终会生成虚拟 DOM (VNode)。对于带有事件监听器的 VNode,Vue 会在适当的时机,将事件监听器绑定到真实的 DOM 元素上。

@click="handleClick" 为例,渲染函数中可能会包含类似这样的代码:

// 假设的渲染函数片段
function render() {
  return h(
    'button',
    {
      onClick: handleClick  // 注意这里是 onClick,不是 @click
    },
    '点我'
  );
}

这里 h 是 Vue 3 提供的 createElement 函数,用于创建 VNode。可以看到,@click 被转换成了 onClick,并作为属性传递给了 button 标签对应的 VNode。

在 VNode 被 patch 到真实 DOM 时,Vue 会将 onClick 对应的 handleClick 函数绑定到 button 元素的 click 事件上。这样,当用户点击按钮时,handleClick 函数就会被执行。

3. 组件自定义事件的注册

对于组件的自定义事件(例如 <MyComponent @custom-event="handleCustomEvent">),情况稍微复杂一些。Vue 需要将父组件的 handleCustomEvent 函数传递给子组件,让子组件在适当的时机调用它。

这通常通过 props 来实现。Vue 会将事件监听器函数作为 props 传递给子组件。例如,上面的例子中,MyComponent 可能会收到一个名为 onCustomEventprops

// MyComponent 的 props 定义
props: {
  onCustomEvent: {
    type: Function,
    default: null
  }
}

子组件在需要触发 custom-event 时,会调用 this.onCustomEvent (在setup中可能是 props.onCustomEvent)。

二、事件派发:城堡的“投石车”

当组件内部发生了一些事情,需要通知父组件或者其他组件时,就需要“派发”事件。在 Vue 中,我们使用 emit 方法来派发事件。

1. emit 方法的本质

emit 方法本质上就是一个函数调用,它会查找组件实例上注册的事件监听器,并依次执行这些监听器函数。

例如,在 MyComponent 中,我们可以这样派发 custom-event

// 在 setup 函数中
setup(props, { emit }) {
  const handleClick = () => {
    // ... 一些操作
    emit('custom-event', 'Hello from MyComponent!'); // 派发 custom-event
  };

  return {
    handleClick
  };
}

// 或者 在 template 中
<template>
  <button @click="$emit('custom-event', 'Hello from MyComponent!')">点击触发事件</button>
</template>

emit('custom-event', 'Hello from MyComponent!') 的作用是:

  1. 找到当前组件实例上所有监听 custom-event 的函数。
  2. 依次调用这些函数,并将 'Hello from MyComponent!' 作为参数传递给它们。

2. 如何查找事件监听器?

Vue 在组件实例上维护了一个事件监听器列表。这个列表通常是一个对象,key 是事件名称,value 是一个函数数组。例如:

// 组件实例上的事件监听器列表
instance.emitsOptions = {
  'custom-event': null // 表明这是一个有效的事件
}

instance.props = {
    onCustomEvent: () => { console.log('我是父组件的事件处理函数')}
}

instance.emit = (event, ...args) => {
    // 1. 检查 emitsOptions,确认事件是否有效
    if (instance.emitsOptions && !instance.emitsOptions[event]) {
        console.warn(`事件 ${event} 没有在 emitsOptions 中声明`);
        return;
    }

    // 2. 查找 props 中对应的 onXxx 函数
    const handlerName = `on${event.charAt(0).toUpperCase() + event.slice(1)}`
    const handler = instance.props[handlerName];

    // 3. 如果找到,则调用该函数
    if (handler) {
        handler(...args);
    }
}

当我们使用 @custom-event="handleCustomEvent" 注册事件时,Vue 会将 handleCustomEvent 函数添加到这个列表中。当调用 emit('custom-event', ...) 时,Vue 就会从这个列表中找到 handleCustomEvent 函数并执行它。

3. 事件参数的传递

emit 方法可以传递任意数量的参数。这些参数会被传递给事件监听器函数。

例如:

// 子组件
<template>
  <button @click="handleClick">派发事件</button>
</template>

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

export default defineComponent({
  setup(props, { emit }) {
    const handleClick = () => {
      emit('my-event', '参数1', 123, { name: '张三' });
    };

    return {
      handleClick
    };
  }
});
</script>
// 父组件
<template>
  <MyComponent @my-event="handleMyEvent"></MyComponent>
</template>

<script>
import { defineComponent } from 'vue';
import MyComponent from './MyComponent.vue';

export default defineComponent({
  components: {
    MyComponent
  },
  setup() {
    const handleMyEvent = (arg1, arg2, arg3) => {
      console.log('参数1:', arg1); // 参数1: 参数1
      console.log('参数2:', arg2); // 参数2: 123
      console.log('参数3:', arg3); // 参数3: { name: '张三' }
    };

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

在上面的例子中,emit('my-event', '参数1', 123, { name: '张三' }) 传递了三个参数。handleMyEvent 函数会依次接收到这三个参数。

三、emits 选项:让事件更规范

Vue 3 引入了 emits 选项,用于声明组件会派发哪些事件。

1. 声明事件的意义

  • 文档化: emits 选项可以清晰地表明组件会派发哪些事件,方便其他开发者了解组件的功能。
  • 类型检查: Vue 可以根据 emits 选项对事件名称进行类型检查,防止拼写错误。
  • 避免属性冲突: emits 声明的事件,不会被当做 props 传递给组件的根元素,避免了属性冲突。

2. 如何使用 emits 选项

emits 选项可以是一个数组或一个对象。

  • 数组: 用于简单地声明事件名称。

    export default {
      emits: ['custom-event', 'another-event']
    };
  • 对象: 用于更详细地配置事件,例如定义事件参数的验证规则。

    export default {
      emits: {
        'custom-event': (payload) => {
          // 验证 payload 是否符合要求
          return typeof payload === 'string';
        }
      }
    };

    如果 emits 选项是一个对象,并且事件的值是一个函数,那么这个函数会被用作事件参数的验证器。如果验证失败,Vue 会发出警告。

3. 例子

// MyComponent.vue
<template>
  <button @click="handleClick">派发事件</button>
</template>

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

export default defineComponent({
  emits: {
    'my-event': (payload) => {
      return typeof payload === 'string'; // 验证 payload 必须是字符串
    }
  },
  setup(props, { emit }) {
    const handleClick = () => {
      emit('my-event', 'Hello!'); // 派发事件,参数符合要求
      //emit('my-event', 123); // 派发事件,参数不符合要求,会发出警告
    };

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

在这个例子中,emits 选项声明了 my-event 事件,并定义了它的验证规则:payload 必须是字符串。如果 emit('my-event', 123),Vue 会发出警告。

四、事件修饰符:给“石头”加点料

Vue 提供了一些事件修饰符,用于改变事件的行为。这些修饰符可以简化代码,提高开发效率。

1. 常用修饰符

修饰符 作用
.stop 阻止事件冒泡。相当于调用 event.stopPropagation()
.prevent 阻止事件的默认行为。相当于调用 event.preventDefault()
.capture 使用 capture 模式监听事件。在捕获阶段触发事件监听器。
.self 只当事件是从侦听器绑定的元素本身触发时才触发回调。
.once 事件只触发一次。
.passive 以 passive 的方式监听事件。用于优化移动端性能。
.right (点击事件) 只当点击鼠标右键时触发。
.middle (点击事件) 只当点击鼠标中键时触发。
.left (点击事件) 只当点击鼠标左键时触发。
.window 将事件监听器绑定到 window 对象上。
.document 将事件监听器绑定到 document 对象上。
.outside (自定义修饰符,需要自己实现) 只当事件发生在绑定元素之外时触发。
.debounce (自定义修饰符,需要自己实现) 对事件进行防抖处理。
.throttle (自定义修饰符,需要自己实现) 对事件进行节流处理。
.number 如果事件触发于一个 <input> 元素上,并且该元素的 type 属性是 numberrange,那么会将事件对象中的 target.value 转换为数字类型。
.trim 如果事件触发于一个 <input> 元素上,那么会将事件对象中的 target.value 去除首尾空格。

2. 例子

<template>
  <div @click="handleDivClick">
    <button @click.stop="handleButtonClick">点我</button>
  </div>
  <a href="https://www.example.com" @click.prevent="handleLinkClick">点击阻止跳转</a>
  <input type="text" @input.trim="handleInput">
</template>

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

export default defineComponent({
  setup() {
    const handleDivClick = () => {
      console.log('Div clicked');
    };

    const handleButtonClick = () => {
      console.log('Button clicked');
    };

    const handleLinkClick = () => {
      console.log('Link clicked, but navigation prevented');
    };

    const handleInput = (event) => {
      console.log('Input value:', event.target.value);
    };

    return {
      handleDivClick,
      handleButtonClick,
      handleLinkClick,
      handleInput
    };
  }
});
</script>

在上面的例子中:

  • .stop 阻止了按钮点击事件冒泡到 div 元素。
  • .prevent 阻止了链接的默认跳转行为。
  • .trim 去除了输入框值的首尾空格。

3. 自定义事件修饰符

Vue 允许我们自定义事件修饰符。这可以让我们封装一些常用的事件处理逻辑,提高代码的可维护性。

自定义事件修饰符需要通过 app.directive 来注册。

// main.js
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

app.directive('debounce', {
  mounted(el, binding) {
    let timer = null;
    el.addEventListener('input', (event) => {
      if (timer) {
        clearTimeout(timer);
      }
      timer = setTimeout(() => {
        binding.value(event);
      }, 500); // 500ms 防抖
    });
  }
});

app.mount('#app');
// App.vue
<template>
  <input type="text" v-debounce="handleInput">
</template>

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

export default defineComponent({
  setup() {
    const handleInput = (event) => {
      console.log('Input value after debounce:', event.target.value);
    };

    return {
      handleInput
    };
  }
});
</script>

在这个例子中,我们定义了一个 v-debounce 指令,用于对 input 事件进行防抖处理。这样,我们就可以在模板中使用 v-debounce 来简化代码。

五、总结:事件机制的“城堡蓝图”

Vue 3 的事件机制就像一套精密的城堡蓝图:

  1. 事件注册: 给城堡安上“监听器”,监听来自外部的信号。
  2. 事件派发: 使用“投石车”向其他城堡发送消息。
  3. emits 选项: 规范城堡间的通信协议,避免误解。
  4. 事件修饰符: 给“石头”加点料,改变消息的传递方式。

理解了这套蓝图,我们就能更好地利用 Vue 3 的事件机制,构建出更加灵活、可维护的组件。

今天就到这里,希望大家有所收获!下次有机会再跟大家聊聊 Vue 3 的其他有趣特性。 谢谢大家!

发表回复

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