六维教程

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 事件对象,有 targetpreventDefault 那些;而组件自定义事件(@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"
受控输入组件 propsvalue + emit('update', 参数)
上一篇
Vue3 组件通信入门
下一篇
Vue3 组件 v-model