主题
Vue3 入门(二)
本页对应 Vue3 入门,共 7 段视频,约 1 小时 17 分。
学习目标
- 能说明 Vue3 组合式 API 下生命周期函数的命名与使用方式,并正确使用
onMounted、onUnmounted等完成挂载后请求、定时器清理等任务。 - 能在
<script setup>中使用defineProps实现父传子,传递静态数据和响应式数据,并在模板与脚本中正确访问。 - 能使用
defineEmits实现子传父,绑定并触发自定义事件,将子组件数据传递给父组件处理。 - 能使用
ref获取 DOM 元素或组件实例,理解获取时机需在挂载完成后,并使用defineExpose显式暴露组件属性或方法。 - 能使用
provide和inject完成跨层组件通信,传递普通数据、响应式数据和方法,并遵守单向数据流原则。 - 能综合运用组合式 API 完成列表渲染、删除和编辑功能,包括接口调用、弹框回填、更新列表及父子组件事件通信。
知识点
本节要解决什么问题
这一节从“认识 Vue3”出发,把组合式 API 下最常用的几块内容串起来:生命周期、父子通信、模板引用、跨层通信,以及一个表格综合案例。学习时不要只记写法,而要抓住一个主线:Vue2 里的开发思想大多还在,变化主要集中在组合式 API 的写法上。下面按知识点拆开。
生命周期函数:先对照,再记规律
为什么需要生命周期?因为组件从创建、挂载到卸载,不同阶段要做不同的事,例如发请求、清定时器。组合式 API 下,beforeCreate 和 created 不再单独写,转由 setup 承担;其余钩子基本是“on + 原名称”,如 onMounted、onBeforeUnmount、onUnmounted。怎么想?先记对照关系,再用最熟悉的 onMounted 验证。怎么做:
js
import { onMounted } from 'vue'
onMounted(() => {
console.log('组件挂载完毕')
})转写稿中通过保存代码观察控制台,确认组件挂载后会自动执行回调;又连续注册多个 onMounted,打印顺序为 1、2,插入第三个后为 1、3、2,说明多次调用按注册顺序执行。易错点:钩子要从 vue 导入;卸载阶段不再叫 destroyed,而是 onUnmounted;不要把它写成普通函数随便调用。
父传子:属性绑定不变,接收改用 defineProps
为什么需要?父组件把数据交给子组件是最常见的通信方式。怎么想?仍然是两步:父组件在子组件标签上绑定属性,子组件声明接收。怎么做:
js
// 父组件
<Child message="father.Message" :count="count" />
// 子组件
const props = defineProps({
message: String,
count: Number
})模板中可直接使用 message;脚本中通过 props.message 访问。传响应式数据时,父组件要写 :count="count",子组件用 Number 接收。转写稿中刷新页面能看到“父组件传入的数据”,并在脚本里打印 props 验证。易错点:defineProps 是编译宏,不需要导入;属性名要一致;静态字符串和动态绑定要区分冒号。
子传父:事件模型不变,多一步 defineEmits
为什么需要?子组件发生点击、提交等行为后,要通知父组件。怎么想?仍是“父绑定事件、子触发事件”。怎么做:
js
// 父组件
<Child @get-message="getMessage" />
// 子组件
const emit = defineEmits(['get-message'])
emit('get-message', 'this is message')转写稿中点击按钮后,父组件打印出 this is message,证明数据已传出。易错点:defineEmits 也是编译宏,不用导入;事件名要与父组件监听名一致;第二个参数才是要传的数据。
模板引用:ref 标识 + 挂载后访问
为什么需要?有时要直接拿 DOM 或子组件实例,例如打开弹窗、调用子组件方法。怎么想?先用 ref() 生成容器,再在标签上写同名 ref,最后在挂载后取 .value。怎么做:
js
const h1 = ref(null)
onMounted(() => {
console.log(h1.value)
})html
<h1 ref="h1">标题</h1>组件实例默认不暴露内部属性和方法,需要子组件用 defineExpose({ open }) 显式抛出。转写稿中先获取 DOM,再获取组件实例,并验证只有暴露后才能拿到。易错点:必须在挂载完成后访问;组件实例不是所有内容都能直接拿。
provide / inject:跨层通信
为什么需要?顶层组件要把数据或方法传给深层组件,逐层 props 太麻烦。怎么想?顶层“提供”,底层“注入”,靠同一个 key 对应。怎么做:
js
// 顶层
provide('dataKey', ref(0))
provide('setCountKey', setCount)
// 底层
const count = inject('dataKey')
const setCount = inject('setCountKey')转写稿中用定时器把 count 改成 100,验证响应式;又通过传递方法让底层间接修改顶层数据,保持“谁的数据谁负责修改”。易错点:key 要一致;传响应式数据要传 ref 对象;不要破坏单向数据流。
综合案例:列表、删除、编辑
列表渲染仍是“声明响应式数据 → 调接口 → 赋值 → 绑定模板”:
js
const list = ref([])
const getList = async () => {
const res = await axios.get('/list')
list.value = res.data
}
onMounted(() => getList())删除时通过表格插槽拿到当前行 id,调删除接口后重新 getList() 更新列表。编辑功能在子组件中通过 defineExpose 暴露 open 方法,父组件用模板引用调用 open(row) 打开弹窗并回填;确认时调接口、关闭弹窗,再 emit 通知父组件刷新列表。转写稿中调试时出现过 axios is not defined,补上导入后接口正常。易错点:接口字段要与表单字段一致;删除、更新后要刷新列表;mock 接口可在 Network 面板核对参数和返回值。
小结
Vue3 组合式 API 并没有推翻原有组件通信思想:父传子仍是属性,子传父仍是事件,跨层仍靠提供与注入;变化在于 defineProps、defineEmits、defineExpose 等编译宏,以及生命周期钩子的 on 写法。把“为什么需要、怎么想、怎么做、易错点”连起来,比死记语法更稳。
从选项式API的Counter看问题
为什么需要先看旧写法?因为组合式API的“体验”不是背概念,而是和原来的选项式写法对照,才能看出变化到底发生在哪里。转写稿里反复强调“先对比、再落地”,本节也按这个思路来。
怎么想:选项式API把组件的不同能力分门别类放进 data、methods 等选项里。计数案例中,状态 count 属于数据,修改方法 addCount 属于方法,二者被拆在两个选项中。
怎么做:先保留笔记中的选项式Counter:
vue
<script>
export default {
data(){
return {
count:0
}
},
methods:{
addCount(){
this.count++
}
}
}
</script>易错点:这里访问状态要写 this.count;count 必须先在 data 的返回值里声明,否则模板和方法里都拿不到。不要因为案例简单就忽略“状态归data、行为归methods”的组织方式,这正是后面要对比的起点。
换成组合式API的Counter
为什么需要换?因为同一个计数功能,在选项式写法里被拆成“数据区”和“方法区”。功能一复杂,同一个业务逻辑可能散落在多个选项里,阅读时要上下跳。组合式API想解决的就是“按逻辑组织代码”,而不是按选项类型组织代码。
怎么想:把“计数状态”和“修改计数的函数”看成一组逻辑,放在一起。ref(0) 负责创建响应式计数,addCount 负责修改它。脚本里通过 count.value 读写,模板里则可以直接使用 count。
怎么做:笔记中的组合式Counter如下:
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
const addCount = ()=> count.value++
</script>易错点:第一,ref 必须从 vue 导入;第二,ref(0) 得到的是响应式对象,脚本中修改要写 count.value++,不能漏掉 .value;第三,模板中通常直接写 count,Vue 会自动解包,不要机械地在模板里也写 count.value;第四,<script setup> 顶层声明的 count、addCount 可直接给模板使用,不需要再 return。
“代码量变少”到底少在哪
为什么需要关注代码量?因为初学者容易把“少写代码”误解成“功能变少”。这里的少,是少了选项式结构中的样板层级。
怎么想:选项式写法要写 export default、data()、return、methods 等外壳;组合式写法把状态和函数直接声明在 <script setup> 顶层。功能没少,壳少了。
怎么做:对照两个代码块即可:count 从 data 的返回值中拿出来,变成 const count = ref(0);addCount 从 methods 对象中拿出来,变成普通函数。迁移时可以先保留原逻辑,只改“存放位置”和“访问方式”。
易错点:不要为了追求短而把变量名写乱;ref 创建的响应式对象和普通数字不是一回事。转写稿中多处出现 .value,正是提醒脚本环境与模板环境不同。调试时可以先点击按钮看数字是否递增,再在脚本中打印 count.value 观察变化;这比只看代码更容易发现漏写 .value 的问题。
“分散式维护”变成“集中式维护”
为什么需要集中?因为真实业务不是只有一个 count。转写稿里讲生命周期时提到,组合式API下生命周期函数要“先导入、再执行、传入回调”,例如 onMounted 导入后执行回调,组件挂载完毕自动执行;多次调用时按注册顺序执行,转写稿还通过打印1、2以及插入3验证了顺序。这说明组合式API的组织方式是“把相关逻辑写成一组函数调用”,而不是散落到各个选项里。
怎么想:Counter里,count 和 addCount 是同一组逻辑,所以放在一起;以后有“请求列表”的逻辑,就把列表状态、请求方法、onMounted 调用放在相邻位置。这样维护时看一块,而不是在 data、methods、mounted 之间来回找。
怎么做:本节先做到“状态和修改函数相邻声明”。如果继续扩展,可以按逻辑分组,例如计数逻辑一组、其他逻辑另一组;不要把所有变量和函数无脑堆在一起。
易错点:集中式维护不等于“全部塞进一个文件就完事”。转写稿中多次调用生命周期函数会按注册顺序执行,这提醒我们:函数调用有顺序,逻辑分组也要有边界。迁移旧代码时,不要一边改写法一边改业务,先把 data 改成 ref、methods 改成函数、脚本访问补 .value,再验证点击递增,最后再调整分组。
小结:体验组合式API的检查清单
为什么需要检查清单?因为Counter太小,容易“看懂了但写不对”。怎么想:按“导入—声明—修改—验证”四步检查。怎么做:确认 import { ref } from 'vue' 存在;确认 const count = ref(0);确认 addCount 中使用 count.value++;确认模板绑定 addCount 后点击能递增。易错点:漏导入、漏 .value、在模板误写 .value、把 ref 对象当普通数字直接 count++。本节不涉及图片;两个代码块就是核心对照材料。先把这组最小案例写顺,再进入生命周期、父子通信等内容,才不会把“组合式API”只记成几个新函数名。
本节要解决的问题

这一节围绕 Vue3 组合式 API 的“更多优势”展开:生命周期函数、父子通信、模板引用、跨层通信,以及一个综合小案例。学习时不要只记 API,而要抓住每条链路:为什么需要它、思路是什么、代码怎么写、哪里容易错。
生命周期函数
为什么需要:组件从创建到销毁有不同阶段,发送请求、清理定时器等逻辑要在合适时机自动执行。
怎么想:Vue3 组合式 API 中,beforeCreate、created 由 setup 替代;其余钩子基本是“加 on 前缀”,如 mounted 变 onMounted,卸载相关变为 onBeforeUnmount、onUnmounted。
怎么做:先导入,再执行并传入回调。
js
import { onMounted } from 'vue'
onMounted(() => {
console.log('组件挂载完毕')
})同一个生命周期函数可多次调用,执行顺序按注册顺序依次执行;接别人代码时,可在外部再注册一个钩子补充逻辑,避免直接改原逻辑。
易错点:组合式 API 下没有 onCreated 这种写法;卸载完毕不是 destroyed,而是 onUnmounted。
父传子
为什么需要:父组件的数据要传给子组件渲染,这是最常用通信方式。
怎么想:父组件绑定属性不变;子组件接收方式变为 defineProps。
怎么做:
vue
<!-- 父组件 -->
<Child :message="fatherMessage" :count="count" />
<!-- 子组件 -->
<script setup>
const props = defineProps({
message: String,
count: Number
})
console.log(props)
</script>传响应式数据时,父组件用 : 绑定 ref 数据;子组件声明对应字段和类型。
易错点:defineProps 是编译器宏,不需要手动引入;接收字段名要和父组件绑定属性一致;在 script setup 中使用 props 要接返回值。
子传父
为什么需要:子组件发生点击、提交等行为后,要通知父组件更新数据。
怎么想:仍然是事件模型:父组件绑定事件,子组件触发事件。Vue3 多了一步:用 defineEmits 生成 emit。
怎么做:
vue
<!-- 父组件 -->
<Child @get-message="getMessage" />
<!-- 子组件 -->
<script setup>
const emit = defineEmits(['get-message'])
const send = () => {
emit('get-message', 'this is message')
}
</script>易错点:defineEmits 接收数组,声明当前组件可触发的事件;emit 第一个参数是事件名,第二个参数才是数据;不要把它和 defineProps 混用。
模板引用
为什么需要:有时要直接拿到真实 DOM 或子组件实例,例如打开弹框、调用子组件方法。
怎么想:先用 ref 创建容器,再通过模板上的 ref 标识绑定,组件挂载完成后从 .value 取。
怎么做:
vue
<script setup>
import { ref, onMounted } from 'vue'
const h1Ref = ref(null)
const comRef = ref(null)
onMounted(() => {
console.log(h1Ref.value)
console.log(comRef.value)
})
</script>
<template>
<h1 ref="h1Ref">标题</h1>
<TestCom ref="comRef" />
</template>子组件默认封闭内部属性和方法,需要 defineExpose 显式暴露。
易错点:必须在挂载完成后获取;子组件没暴露时,父组件拿不到内部方法;模板 ref 名要和变量名对应。
provide 与 inject
为什么需要:顶层组件向任意深层组件传数据,避免一层层 props。
怎么想:顶层 provide 提供,底层 inject 接收,靠同一个 key 对应。
怎么做:
js
// 顶层
provide('data-key', 'room data')
provide('count-key', count)
provide('set-count-key', setCount)
// 底层
const data = inject('data-key')
const count = inject('count-key')
const setCount = inject('set-count-key')传响应式数据时,第二个参数放 ref 对象;底层要修改顶层数据,不要直接改,而是接收顶层传下来的方法,由方法修改。
易错点:key 要一一对应;跨层通信不代表可以破坏单向数据流,谁的数据谁负责修改。
综合案例:列表、删除、编辑
为什么需要:用真实业务串起组合式 API。
怎么想:列表先声明响应式数据,再调接口赋值,最后绑定表格;删除先取当前行 ID,再调删除接口,最后刷新列表;编辑按“打开弹框—回填数据—更新数据”推进。
怎么做:
js
const list = ref([])
const getList = async () => {
const res = await axios.get('/list')
list.value = res.data
}
onMounted(() => getList())
const del = async (id) => {
await axios.delete(`/del/${id}`)
getList()
}编辑弹框可通过子组件 defineExpose 暴露 open 方法,父组件用模板引用调用;确认更新时调接口、关闭弹框,并通过 emit 通知父组件刷新列表。
易错点:删除后要更新列表;编辑回填字段要和表单字段一致;使用 axios 前要导入;弹框开关和列表刷新要分清父子职责。
课件图示








代码示例
1. 组合式 API 生命周期函数(onMounted / onUnmounted)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Vue3 组合式API - 生命周期函数</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<button @click="show = !show">切换子组件</button>
<child v-if="show"></child>
</div>
<script>
const { createApp, ref, onMounted, onUnmounted } = Vue
const Child = {
template: `<div>我是子组件</div>`,
setup() {
// 第一步:导入(上面解构)+ 第二步:执行并传入回调
onMounted(() => {
console.log('组件挂载完毕 onMounted 1')
})
onMounted(() => {
console.log('组件挂载完毕 onMounted 2')
})
// 组件卸载完毕时执行(对应 V2 的 destroyed,名字变了)
onUnmounted(() => {
console.log('组件卸载完毕 onUnmounted')
})
return {}
}
}
createApp({
components: { Child },
setup() {
const show = ref(true)
return { show }
}
}).mount('#app')
</script>
</body>
</html>关键行与易错点
- 用法固定两步:先从
vue中import { onMounted },再执行该函数并传入回调,回调会在时机成熟时自动执行。 - 命名规律:
beforeMount → onBeforeMount、mounted → onMounted、unmounted → onUnmounted,即“名字前加on”。 - 没有
onBeforeCreate/onCreated:原beforeCreate、created中的逻辑直接写进setup即可。 - 同一个钩子可被多次调用,按注册顺序依次执行(先注册先执行);所以接手别人的代码时,可以在外面再写一个
onMounted补充自己的逻辑,不动原有代码。 - 卸载钩子叫
onUnmounted,不是onDestroyed,这是 V2 → V3 的明显改名点。
2. 组合式 API 父传子 + 子传父(props / emit)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Vue3 组合式API - 父子通信</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h3>父组件</h3>
<p>父组件收到子组件的消息:{{ message }}</p>
<!-- 父传子:给子组件标签绑定属性,响应式数据要加冒号 -->
<!-- 子传父:@ 绑定自定义事件,携带父组件中的函数 -->
<child
:message="fatherMessage"
:count="count"
@get-message="getMessage"
></child>
</div>
<script>
const { createApp, ref, onMounted } = Vue
const Child = {
// 子传父:需要先声明要触发的自定义事件
emits: ['get-message'],
props: {
message: String,
count: Number
},
template: `
<div>
<p>子组件收到父组件数据:{{ message }} / {{ count }}</p>
<button @click="sendMsg">触发自定义事件</button>
</div>
`,
setup(props, { emit }) {
// props 对象里存放的就是父组件传下来的所有数据
console.log('props ->', props)
const sendMsg = () => {
// 等价于 V2 的 this.$emit('get-message', 数据)
emit('get-message', 'this is message')
}
return { sendMsg }
}
}
createApp({
components: { Child },
setup() {
const fatherMessage = ref('father message')
const count = ref(100)
const message = ref('')
// 父组件中的函数,用形参等待子组件传来的数据
const getMessage = (msg) => {
message.value = msg
}
// 验证父传子的响应式:3 秒后修改 count
onMounted(() => {
setTimeout(() => {
count.value = 200
}, 3000)
})
return { fatherMessage, count, message, getMessage }
}
}).mount('#app')
</script>
</body>
</html>关键行与易错点
- 父传子第一步在父组件模板上绑定属性(写法与 V2 完全一致);第二步在子组件中接收:
<script setup>下用defineProps({ message: String, count: Number })编译器宏,本示例用选项式props+setup(props)的等价写法。 - 传响应式数据时,模板绑定必须加冒号
:count="count",否则传过去的是字符串"count"而不是变量值。 - 在
setup脚本里想用父传下来的数据,需要const props = defineProps(...)接住返回值,props.xxx取值;模板中可直接写xxx。 - 子传父仍是事件模型,但多一步:
<script setup>下必须先用const emit = defineEmits(['get-message'])生成 emit 方法,再emit('事件名', 数据);defineProps/defineEmits是编译器宏,不需要 import,也不能 import。 - 事件名在
defineEmits里写get-message(kebab-case),模板上用@get-message与之对应,大小写不要写错。
3. provide / inject 跨层通信(含传方法修改顶层数据)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Vue3 组合式API - provide / inject</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<room-page></room-page>
</div>
<script>
const { createApp, ref, provide, inject } = Vue
/* ---------- 底层组件 ---------- */
const RoomMessageCom = {
template: `
<div style="border:1px solid #4a90d9; padding:8px; margin-top:8px">
<p>底层组件:来自顶层组件的数据为 {{ roomData }}</p>
<p>底层组件:count 为 {{ countData }}</p>
<button @click="setCount">修改顶层组件的数据</button>
</div>
`,
setup() {
// 通过相同的 key 取出顶层组件提供的数据 / 方法
const roomData = inject('data-key')
const countData = inject('count-key')
const setCount = inject('set-count-key')
return { roomData, countData, setCount }
}
}
/* ---------- 中间层组件(只做透传,不参与通信) ---------- */
const RoomMessageItem = {
components: { RoomMessageCom },
template: `
<div style="border:1px solid #ccc; padding:8px; margin-top:8px">
中间层组件
<room-message-com />
</div>
`
}
/* ---------- 顶层组件 ---------- */
const RoomPage = {
components: { RoomMessageItem },
template: `
<div style="border:1px solid #f56c6c; padding:8px">
顶层组件
<room-message-item />
</div>
`,
setup() {
const count = ref(0)
// 提供普通(静态)数据
provide('data-key', 'this is room data')
// 提供响应式数据:第二个参数必须是 ref 对象
provide('count-key', count)
// 谁的数据谁负责改:把方法传下去,底层调方法间接修改
const setCount = () => {
count.value++
}
provide('set-count-key', setCount)
return {}
}
}
createApp({ components: { RoomPage } }).mount('#app')
</script>
</body>
</html>关键行与易错点
- 两步走:顶层组件
provide('key', 数据)提供,底层组件inject('key')获取,key 必须一一对应,否则取到undefined。 - 想要跨层传递的数据保持响应式,第二个参数必须传
ref对象(provide('count-key', count)),底层拿到后模板里直接写countData,脚本里要写countData.value。 - 遵守单向数据流:底层不能直接改顶层传下来的数据,而是由顶层
provide一个修改方法(如setCount),底层inject后调用它,间接完成修改。 - 顶层 / 底层是相对概念:同一棵组件树里可以存在多组顶层-底层关系,跨两层、三层都能用,中间层组件完全不用写任何透传代码。
- 注意
provide/inject只能在setup中调用,且provide必须在inject之前的父级执行链上完成。
常见错误
模板引用拿到
undefined现象:onMounted里打印h1Ref.value是undefined,或直接写在<script setup>顶层同步代码里永远拿不到。 原因:模板引用只在组件挂载完成后才被赋值,顶层同步代码执行时 DOM 还没生成。 修法:把读取逻辑放进onMounted(或之后触发的交互回调)里;需要等待子组件内部渲染时用nextTick。defineProps/defineEmits/defineExpose被 import 进来 现象:控制台报 “defineProps is not defined” 或编译报错“宏不能被导入”。 原因:把它们当成普通函数,从vue里 import,破坏了编译器宏的识别。 修法:删除 import,直接裸调用;同时注意这些宏不能赋值给变量后再用(如const p = defineProps),也不能在if等非顶层作用域中调用。父组件拿不到子组件的属性/方法 现象:
childRef.value打印出来只有一个带下划线的内部对象,自己写的name、open()都不见了。 原因:<script setup>默认封闭组件内部实现,不对外暴露。 修法:在子组件里显式defineExpose({ open, name }),只暴露必要项。子组件里直接改 props 现象:改了
props.message页面不更新,或控制台出现 “Set operation on key ... failed: target is readonly”。 原因:props 是只读的单向数据流,子组件无权修改父组件数据。 修法:子组件emit事件通知父组件,由数据所有者自己修改;或用props值初始化本地ref副本(并明确后续同步策略)。provide的值在子层不更新 现象:顶层用普通变量(非ref/reactive)provide,之后修改它,底层inject到的仍是旧值。 原因:provide传递的是值的快照,失去响应性追踪。 修法:provide时传ref/reactive对象本身(不要传.value),或在需要只读时传computed/ getter 函数。inject拿不到数据 现象:底层组件inject('xxx')返回undefined。 原因:key 字符串拼写/大小写不一致,或provide所在的组件并不是inject组件的祖先(跨了兄弟分支)。 修法:把 key 抽成共享常量统一引用;确认调用发生在setup顶层同步执行;必要时给inject传默认值兜底。emit事件名对不上,父组件回调不触发 现象:子组件点了按钮,父组件绑定的@get-message没反应,还伴随 “Component emitted event ... but it is neither declared” 警告。 原因:defineEmits里声明的是 camelCase,模板里却写成 kebab-case 之外的拼写;或事件名根本没声明。 修法:defineEmits(['getMessage'])与emit('getMessage', data)严格一致,模板监听写@get-message或@getMessage均可,但字符串必须对得上。
练习
生命周期执行顺序:在一个组件中先后注册三次
onMounted(分别打印 1、3、2 的顺序打乱写),再注册一个onUnmounted,配合v-if切换该组件,记录完整输出顺序,并解释“为什么卸载钩子不能叫onDestroyed”。父子双向通信:父组件维护一个
count(ref),子组件有一个输入框和一个按钮。要求子组件不直接修改父组件的count,而是通过emit把增量回传,由父组件完成累加;同时父组件要把count实时显示在子组件内部。模板引用 + 暴露:封装一个
SearchBox子组件,内部有<input>和focus()方法。父组件点击“聚焦搜索框”按钮时让输入框获得焦点。要求父组件不能直接操作子组件 DOM,且子组件不暴露多余成员。跨层通信:三层组件 A → B → C。A 持有
theme(ref)和toggleTheme()方法,C 需要读取主题并触发切换,B 不做任何数据中转。写出 A、C 两侧代码,并说明如果 C 里想直接theme.value = 'dark'会遇到什么问题。综合改造:把表格案例中的“删除”功能改为先弹确认框再删除,删除成功后刷新列表,并保证删除过程中按钮处于 loading 状态。指出这段逻辑应该分别放在组合式 API 的哪些位置,以及为什么不能在
setup顶层直接调用删除接口。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 组合式API-生命周期函数 | 07:16 |
| 2 | 组合式API下的父子通信-父传子 | 09:28 |
| 3 | 组合式API下的父子通信-子传父 | 07:22 |
| 4 | 组合式API-模版引用 | 08:48 |
| 5 | 组合式API-provide和inject | 15:21 |
| 6 | Vue3综合小案例 | 14:37 |
| 7 | 补充作业-编辑功能作业 | 14:11 |
小结
- Vue3 组合式 API 的迁移主线是“能合并的合并、能省的省”:
beforeCreate/created被setup吸收,destroyed改名为unmounted,其余钩子统一加on前缀——记住这条命名规则比逐个背表更可靠。 - 生命周期钩子是注册式而非覆盖式,同一钩子可注册多次并按注册顺序执行;这既是“不敢动别人代码时追加逻辑”的实用技巧,也意味着排查问题时要注意多段逻辑的叠加影响。
defineProps、defineEmits、defineExpose属于编译期宏而非运行时函数,它们不需要 import、不能随意赋值、只能在顶层调用——把它们当“给编译器的标注”而不是“API 调用”,很多报错自然就说得通了。- 父子通信的模型在 Vue3 中没有变,变的只是写法:属性下行、事件上行依然是铁律;
defineExpose的引入强化了组件封装边界,默认封闭、按需开放。 provide/inject解决的是跨层问题,不是“替代 props”。选择通信方式时先看层级:直接父子用 props/emit,跨多层才考虑 provide/inject,避免为了省事滥用导致数据流向难以追踪。- 响应性在跨层传递时不会自动继承:传值传的是快照,传
ref/reactive才传的是引用。凡是“改了不生效”的问题,先问自己传的到底是值还是容器。 - 单向数据流是底线而非限制:底层组件想改顶层数据,正确姿势是拿到修改方法而不是拿到修改权限——这既保证了数据来源唯一,也让调试时的变更入口可追溯。
- 从选项式到组合式,业务思路基本不变(声明响应数据 → 调接口 → 赋值 → 绑模板),变的只是代码组织方式;迁移时先照着原思路翻译,再考虑抽成可复用的组合函数。