
通信的三个方向
Vue 的数据流是单向的:父组件通过 props 把数据传下去,子组件通过事件把变化报上来。子组件不能直接改父组件的状态。
需要父组件主动调用子组件内部方法时,用 defineExpose 显式暴露。这是第三条路,动手之前先想清楚有没有更合适的做法。
组件上的 v-model 是 props 加事件拼出来的语法糖。
静态传值和动态绑定
不加 : 的属性按 HTML 原生属性处理,值默认是字符串,只能传固定文本。
<Child msg="我是字符串" />需要传数字、布尔、数组、对象或变量时,加 : 或 v-bind。Vue 把它当 JavaScript 表达式求值,值保持原本的类型。
<Child :num="123" :isShow="true" :list="[1, 2, 3]" :user="{ name: 'zs' }" :custom="data"/>这五行分别是数字、布尔、数组、对象和变量,类型都保持原样。
运行时的 props 声明
defineProps 和 defineEmits 是 <script setup> 的编译器宏,不需要 import。运行时声明用对象形式,配置项和组件的 props 选项完全一样。
<script setup>const props = defineProps({ msg: String, title: { type: String, required: true }, num: { type: Number, default: 0, validator: (val) => val > 0 }})</script>| 配置 | 作用 |
|---|---|
type | prop 的数据类型 |
required | 是否必须传入 |
default | 父组件没传时使用的值 |
validator | 自定义校验函数,返回 false 时控制台告警 |
type 可以取 String、Number、Boolean、Array、Object、Date、Function、Symbol、Error。也可以直接写自定义类,这时 Vue 用 instanceof 检查传入的值。
类型声明
TS 项目可以只写类型,不写 type、required、default 这些运行时字段,把校验交给 TypeScript。
<script setup lang="ts">const props = defineProps<{ msg: string num?: number list: number[] user: { name: string } fn: (id: number) => void}>()</script>必传的属性直接写类型,可选的属性加 ?。开发环境下编译器会从类型反推出等价的运行时声明,所以运行时校验照样有。
属性多了以后,抽到 interface 里更清楚。
<script setup lang="ts">interface User { name: string age: number}
defineProps<{ user: User list?: number[]}>()</script>两种声明方式的取舍
| 对比项 | 运行时声明 | 类型声明 |
|---|---|---|
| 适用 | JS 和 TS 都能用 | 需要 TypeScript |
| 校验时机 | 运行时,控制台告警 | 编译期,编辑器直接标红 |
| 类型提示 | 无 | props.xxx 有完整提示 |
| 默认值 | 直接写 default | 3.5 及以上用解构默认值,3.4 及以下用 withDefaults |
两种声明不能同时用,同时写会编译报错。
props 的默认值
Vue 3.5 起,类型声明可以直接用 JavaScript 的解构默认值,不需要额外的宏。
<script setup lang="ts">interface Props { msg?: string labels?: string[]}
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()</script>withDefaults:3.4 及以下的写法
withDefaults 的第一个参数是 defineProps 的结果,第二个参数是默认值对象。
<script setup lang="ts">interface Props { msg?: string num?: number list?: number[]}
const props = withDefaults(defineProps<Props>(), { msg: '默认消息', num: 100, list: () => [1, 2, 3]})</script>它生成等价的运行时 default 选项,同时把返回的 props 类型里已经给了默认值的属性去掉 ?。上面这段代码里,props.num 的类型是 number,不再是 number | undefined。
参考 withDefaults。
引用类型的默认值
数组和对象的默认值必须包在函数里返回。每次求值都会新建一份,每个组件实例拿到独立的数据。
defineProps({ list: { type: Array, default: () => [] }, user: { type: Object, default: () => ({}) }})直接写 list: [1, 2, 3] 会让所有实例共用同一个数组,改一个会影响全部。用 withDefaults 时同理,工厂函数写在第二个参数里。
声明自定义事件
子组件不能直接改父组件的数据。父组件想知道子组件里发生了什么,只能由子组件主动派发一个事件。
defineEmits 用来声明当前组件会派发哪些事件。声明之后,父组件用 @事件名 监听,回调函数收到子组件传出来的参数。
数组写法
数组写法只声明事件名,不约束参数格式,JS 和 TS 项目都能用。
<script setup>const emit = defineEmits(['sendData', 'changeCount'])
const handleClick = () => { emit('sendData', { name: '小明', age: 18 }) emit('changeCount', 100)}</script>
<template> <button @click="handleClick">向父组件传值</button></template><script setup>const getChildData = (data) => { console.log(data) // { name: '小明', age: 18 }}
const handleCount = (num) => { console.log(num) // 100}</script>
<template> <Child @sendData="getChildData" @changeCount="handleCount" /></template>对象写法
对象写法可以在声明事件的同时加上参数校验。
<script setup>const emit = defineEmits({ // 不校验 sendData: null, // 校验 submit 的参数 submit: ({ email, password }) => { if (email && password) { return true } console.warn('submit 的参数不合法') return false }})</script>校验函数返回 false 时,控制台会打印告警。
类型声明写法
类型声明写法约束事件名、参数类型和返回值,参数写错编辑器直接标红。
<script setup lang="ts">// 格式:(事件名:字面量,参数:类型):返回值const emit = defineEmits<{ (e: 'sendData', user: { name: string; age: number }): void (e: 'changeCount', count: number): void (e: 'close'): void}>()
const handleClick = () => { emit('sendData', { name: '小红', age: 20 }) emit('changeCount', 66) emit('close')}</script>事件多的时候可以把参数类型抽出去。Vue 3.3 起还支持具名元组写法,看起来更像一个对象的字段。
<script setup lang="ts">type User = { name: string age: number}
type Emits = { sendData: [user: User] changeCount: [count: number] close: []}
const emit = defineEmits<Emits>()</script>事件名和参数
一次可以派发多个参数,父组件的回调按顺序接收。
// 子组件:一次派发多个参数emit('info', '标题', 200, [1, 2, 3])
// 父组件:回调按顺序接收const getInfo = (title, num, list) => { console.log(title, num, list)}事件名用小驼峰或短横线都行,模板里两种写法可以互相匹配,团队内保持一致即可。
props 是只读的,子组件不能给它重新赋值,控制台会告警。要让父组件改数据,就派发一个事件出去,由父组件自己决定怎么改。
// 不要这样改props.name = 'xxx'
// 应该这样通知父组件emit('update:name', 'xxx')未声明的事件会落到透传属性
组件自定义事件不会冒泡,只有直接父组件能监听到。
父组件写的 @close 如果没在子组件的 emits 里声明,Vue 只把它当普通 attribute,落到子组件的根元素上,等价于在根元素上挂了一个原生 DOM 监听器。
组件上的 v-model
组件上的 v-model 是 props 加事件拼出来的语法糖:父组件传一个 modelValue,子组件在需要更新时派发 update:modelValue。
<script setup>const props = defineProps(['modelValue'])const emit = defineEmits(['update:modelValue'])</script>
<template> <input :value="props.modelValue" @input="emit('update:modelValue', $event.target.value)" /></template><script setup>import { ref } from 'vue'
const text = ref('')</script>
<template> <Child v-model="text" /></template>用 v-model:name 可以换 prop 的名字,一个组件上也能同时绑多个。父组件写 v-model:first-name,子组件对应的是 firstName prop 和 update:firstName 事件。
<UserName v-model:first-name="first" v-model:last-name="last"/>defineModel 从 3.4 起稳定可用(3.3 有实验版本),上面那套 props 加事件可以合并成一行。
<script setup>const model = defineModel()
model.value = 'hello'</script>defineExpose 暴露子组件内部
<script setup> 写法的组件默认关闭:父组件通过模板引用拿到的实例上,看不到组件内部声明的变量、方法和状态。
要在父组件里用,子组件必须用 defineExpose 显式写出暴露哪些内容。没写进去的一律访问不到。
<script setup>import { ref } from 'vue'
const childMsg = ref('我是子组件的消息')
const childMethod = () => { console.log('我是子组件方法')}
defineExpose({ childMsg, childMethod})</script>父组件用一个同名的 ref 接收实例。ref 在挂载完成之后才会绑上,所以访问要放在 onMounted 里,否则拿到的是 null。
<script setup>import { onMounted, ref } from 'vue'import Child from './Child.vue'
const childRef = ref(null)
onMounted(() => { console.log(childRef.value.childMsg) childRef.value.childMethod()})</script>
<template> <Child ref="childRef" /></template>暴露出去的 ref 会保持响应式:子组件里改 childMsg,父组件拿到的值跟着更新。
对比一下 Options API:data 和 methods 都挂在实例上,父组件直接就能拿到。<script setup> 把默认行为反过来,只有显式暴露的才可见。
父组件里 childRef 的类型是 any,拿不到暴露成员的提示。用 TS 时改成下面这样。
<script setup lang="ts">import { onMounted, ref } from 'vue'import Child from './Child.vue'
const childRef = ref<InstanceType<typeof Child> | null>(null)
onMounted(() => { console.log(childRef.value?.childMsg) childRef.value?.childMethod()})</script>参考 defineExpose。
Vue 3 学习笔记
连载中分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 209 天
部分内容可能已过时