defineEmits用法陷阱

1,373 阅读4分钟

昨天尝试在公共子组件中使用defineEmits初始化父组件中传递的方法,由于是公共组件,所以方法名使用的是动态传递过来的,于是想在defineEmits()参数中使用自己定义好的参数,但是报错了,报错信息如图所示

image.png 分析原因,这个警告信息来自于 Vue 3 的编译器插件 @vue/compiler-sfc,它针对的是 <script setup> 特性中的一个常见问题。在 <script setup> 语法中,所有的顶级变量和函数都被视为组件的响应式状态、计算属性或方法,并且它们默认都是组件内部私有的。

defineEmits() 函数用于在 <script setup> 中声明组件可能发出的事件。然而,由于 <script setup> 的特殊编译行为,你不能在 defineEmits() 中直接使用在 <script setup> 范围内局部声明的变量来动态构建事件名列表,因为 defineEmits() 会被提升到 setup() 函数之外的作用域。

举个例子,以下代码会导致警告:

vue复制代码
	<script setup>  

	const someCondition = true;  

	  

	const emit = defineEmits([  

	  someCondition ? 'eventA' : 'eventB' // 这里会触发警告  

	]);  

	</script>

要解决这个问题,你有几个选项:

  1. 直接在 defineEmits() 中硬编码事件名
    如果事件名不依赖于任何局部变量,你可以直接在 defineEmits() 中列出它们。

    vue复制代码
    	<script setup>  
    
    	const emit = defineEmits(['eventA', 'eventB']);  
    
    	</script>
    
  2. 使用单独的 <script> 块
    如果你需要在模块作用域中初始化组件选项,并且这些选项依赖于某些局部变量,那么你可以使用传统的 <script> 块而不是 <script setup>

    vue复制代码
    	<script>  
    
    	export default {  
    
    	  emits: [/* 动态生成的事件名列表 */],  
    
    	  // ... 其他选项 ...  
    
    	}  
    
    	</script>  
    
    	 
    
    	<script setup>  
    
    	// ... 其他的响应式状态、方法等 ...  
    
    	</script>
    

    但是请注意,<script setup> 和 <script> 中的内容在同一个组件中是隔离的,它们之间的共享通常是通过 propscontextattrsslotsemit)或者通过 refreactive 之类的 Vue 响应式 API 进行的。

  3. 在 setup() 函数中动态处理事件
    如果你需要在 setup() 函数中基于某些条件动态地处理事件,你可以直接在 setup() 函数内部使用 context.emit 而不是 defineEmits()。但是请注意,<script setup> 语法中并没有直接暴露 setup() 函数的参数,但你可以通过 defineExpose 或 definePropsdefineEmits 等 API 来模拟类似的行为。

    vue复制代码
    	<script setup>  
    
    	import { defineExpose } from 'vue'  
    
    	 
    
    	const emit = (eventName, ...args) => {  
    
    	  if (someCondition) {  
    
    	    context.emit('eventA', ...args);  
    
    	  } else {  
    
    	    context.emit('eventB', ...args);  
    
    	  }  
    
    	}  
    
    	 
    
    	defineExpose({ emit }) // 如果需要在模板中访问 emit 函数  
    
    	// ... 其他逻辑 ...  
    
    	</script>
    

    但是请注意,在 <script setup> 中直接访问 context 是不被推荐的,因为它破坏了 <script setup> 的简洁性。通常,你应该尽量在 defineEmits() 中静态地列出所有可能的事件。

综上所诉:

如果你希望你的 Vue 组件能够发出未知事件名的事件(即动态事件名),你可以在 emits 选项中使用一个特殊的函数来声明,该函数接收事件名作为参数,并返回一个布尔值来表示是否允许该事件被发出。然而,在 <script setup> 中,defineEmits 并不直接支持这样的动态事件声明。

不过,对于动态事件名的情况,你通常不需要在 emits 选项中显式声明每个可能的事件名,而是可以简单地返回一个 true 或者使用一个通配符(尽管 Vue 官方并不直接支持通配符作为事件名)。在 <script setup> 中,你可以直接使用 emit 函数来发出任何事件名,Vue 将会允许这样做,但你可能会在开发时收到一个警告,提示你未声明该事件。 image.png

如果你希望避免这个警告,并且仍然想使用动态事件名,你可以在传统的 <script> 块中声明一个通用的 emits 选项,然后在 <script setup> 中使用 emit 函数。但是请注意,这并不会真正地为每个动态事件名提供类型检查。

这里是一个示例:

vue复制代码
	<script>  

	// 使用传统的 <script> 块来声明一个通用的 emits 选项  

	export default {  

	  emits: {  

	    // 使用一个函数来允许所有事件名的发出  

	    // 注意:这只是一个示例,并不会为动态事件名提供真正的类型检查  

	    dynamicEvent(event, ...args) {  

	      // 这里可以添加自定义的逻辑来验证事件或参数  

	      // 例如,检查事件名是否以特定前缀开头  

	      // 但通常这里只需返回 true 来允许所有事件  

	      return true;  

	    }  

	  },  

	  // ... 其他选项 ...  

	}  

	</script>  

	  

	<script setup>  

	import { ref } from 'vue'  

	  

	// 在 <script setup> 中使用 emit 函数发出动态事件  

	const condition = ref(true)  

	  

	function handleEvent() {  

	  const eventName = condition.value ? 'openManager' : 'closeManager'  

	  emit(eventName) // 发出动态事件名  

	}  

	</script>  

	  

	<template>  

	  <!-- 模板内容 ... -->  

	</template>

在这个示例中,尽管我们在 emits 选项中定义了一个函数来处理动态事件,但 Vue 实际上并不会为这些动态事件名提供额外的类型检查。这个函数的主要作用是允许所有事件名的发出,并避免在开发时收到未声明事件的警告。

如果你确实需要为每个动态事件名提供类型检查,你可能需要考虑使用其他方法或工具来实现这一点,因为 Vue 自身的 emits 选项并不直接支持动态事件名的类型检查。