Vue3 组件事件
props 是”父给子”往下传;反向的”子给父”靠自定义事件(emit)。
比如子组件是个计数器,点”+1”后父组件得知道数值变了,才能更新自己的数据或发起请求。
子组件发事件
defineEmits 做两件事:
- 声明这个组件会发出哪些事件
- 同时返回一个触发事件的函数,这个函数存进变量,习惯命名为
emit
<!-- 子组件 CounterButton.vue -->
<script setup>
const emit = defineEmits(['increase'])
const add = () => {
emit('increase', 1) // 发出 increase 事件,带上参数 1
}
</script>
<template>
<button @click="add">+1</button>
</template>
defineEmits 可以声明多个事件,事件名都写在数组里:
const emit = defineEmits(['increase', 'decrease'])
const add = () => emit('increase', 1)
const sub = () => emit('decrease', 1) // 同一个 emit,喊不同的事件名
调用 emit('事件名', 参数) 触发,参数数量不限,父组件都能收到。
如果一次想带多个数据,最省心的做法是用一个对象包起来,比如 emit('save', { name, age }),父组件取用也方便。
顺便一提,defineEmits 里的声明不只是形式,不声明就 emit 的事件,Vue 会在开发环境警告,父组件也监听不到。
父组件接收事件
父组件像监听原生事件一样监听:
<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import CounterButton from './components/CounterButton.vue'
const total = ref(0)
const handleIncrease = (step) => {
total.value += step // step 收到子组件传来的 1
}
</script>
<template>
<p>合计:{{ total }}</p>
<CounterButton @increase="handleIncrease" />
</template>
和子组件对照看,两边是严丝合缝的一对:
- 子组件
emit('increase', 1),父组件就写@increase="handleIncrease",事件名increase必须一模一样 emit带的参数1,会原样落进父组件方法handleIncrease的第一个形参step
子组件怎么发,父组件就怎么收。
父组件怎么拿参数
单个参数
接收事件带出的参数,有两种等价写法。
方法写法,参数自动落到形参:
<script setup>
const handleUpdate = (val) => {
keyword.value = val // val 收到子组件传来的 e.target.value
}
</script>
<template>
<ValueInput :value="keyword" @update="handleUpdate" />
</template>
内联写法,不用定义方法,参数就是 $event:
<ValueInput :value="keyword" @update="keyword = $event" />
写法二里的 $event,就是写法一里方法形参 val 收到的那个值,一个东西、两种接法。
注意,组件事件的 $event 不是事件对象。
Vue3 事件处理 里,@click="fn($event)" 的 $event 是原生 DOM 事件对象,有 target、preventDefault 那些;而组件自定义事件(@update)里的 $event,是 emit('update', xxx) 携带出来的那个 xxx 参数,可能是任意值,没有 DOM 方法。
两个 $event 只是名字一样,含义完全不同,别拿原生那套去套组件事件。
多个参数
方法写法里,一个参数对一个形参,依次接收:
<Child @save="handleSave" />
const handleSave = (name, age) => { // 依次接收 name、age
console.log(name, age)
}
但内联写法里 $event 只代表第一个参数,拿不到后面的 age。
所以参数超过一个时,要么用方法写法,要么发一个对象包起来(也就是前面说的 { name, age })。
经典场景:受控表单组件
props + emits 组合起来,就是组件库里”受控组件”的雏形——数据存在父组件,子组件只负责”展示 + 上报变化”:
<!-- 子组件 ValueInput.vue -->
<script setup>
const props = defineProps({ value: String })
const emit = defineEmits(['update'])
const onInput = (e) => {
emit('update', e.target.value)
}
</script>
<template>
<input :value="props.value" @input="onInput" />
</template>
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import ValueInput from './components/ValueInput.vue'
const keyword = ref('')
</script>
<template>
<ValueInput :value="keyword" @update="keyword = $event" />
</template>
这个例子把本篇的东西串起来了:
子组件用 props 接 value 展示,把用户输入通过 emit('update', ...) 上报,父组件用 @update 接住再写回 keyword。
速查卡片
| 场景 | 写法 |
|---|---|
| 声明事件 | const emit = defineEmits(['update']) |
| 触发事件带参 | emit('update', 参数) |
| 父组件监听 | <Child @update="handler" /> |
| 内联接收参数 | @update="keyword = $event" |
| 受控输入组件 | props 接 value + emit('update', 参数) |