Skip to content

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更多优势示意图

这一节围绕 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 前要导入;弹框开关和列表刷新要分清父子职责。

课件图示 ​

课件第35页:总结 总结 1. 组合式API中生命周期函数的格式是什么? on + 生命周期名

课件第51页:总结 总结 1. provide和inject的作用是什么? 跨层组件通信 2.

课件第37页:基本思想 1. 父组件中给子组件绑定属性 2. 子组件内部通过props选项接收

课件第4页:Vue3更多的优势 ue 更快的速度 更优的数据响应式 更容易维护 更小的体积

课件第38页:基本思想 1. 父组件中给子组件标签通过@绑定事件 2. 子组件内部通过 $em

课件第32页:Vue3的生命周期API (选项式 VS 组合式) 选项式API 组合式API

课件第39页:总结 总结 父传子 1. 父传子的过程中通过什么方式接收props? defin

课件第3页:Vue3组合式API体验 通过一个 Counter案例 体验Vue3新引入的组合

代码示例 ​

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 之前的父级执行链上完成。

常见错误 ​

  1. 模板引用拿到 undefined 现象:onMounted 里打印 h1Ref.value 是 undefined,或直接写在 <script setup> 顶层同步代码里永远拿不到。 原因:模板引用只在组件挂载完成后才被赋值,顶层同步代码执行时 DOM 还没生成。 修法:把读取逻辑放进 onMounted(或之后触发的交互回调)里;需要等待子组件内部渲染时用 nextTick。

  2. defineProps / defineEmits / defineExpose 被 import 进来 现象:控制台报 “defineProps is not defined” 或编译报错“宏不能被导入”。 原因:把它们当成普通函数,从 vue 里 import,破坏了编译器宏的识别。 修法:删除 import,直接裸调用;同时注意这些宏不能赋值给变量后再用(如 const p = defineProps),也不能在 if 等非顶层作用域中调用。

  3. 父组件拿不到子组件的属性/方法 现象:childRef.value 打印出来只有一个带下划线的内部对象,自己写的 name、open() 都不见了。 原因:<script setup> 默认封闭组件内部实现,不对外暴露。 修法:在子组件里显式 defineExpose({ open, name }),只暴露必要项。

  4. 子组件里直接改 props 现象:改了 props.message 页面不更新,或控制台出现 “Set operation on key ... failed: target is readonly”。 原因:props 是只读的单向数据流,子组件无权修改父组件数据。 修法:子组件 emit 事件通知父组件,由数据所有者自己修改;或用 props 值初始化本地 ref 副本(并明确后续同步策略)。

  5. provide 的值在子层不更新 现象:顶层用普通变量(非 ref/reactive)provide,之后修改它,底层 inject 到的仍是旧值。 原因:provide 传递的是值的快照,失去响应性追踪。 修法:provide 时传 ref/reactive 对象本身(不要传 .value),或在需要只读时传 computed / getter 函数。

  6. inject 拿不到数据 现象:底层组件 inject('xxx') 返回 undefined。 原因:key 字符串拼写/大小写不一致,或 provide 所在的组件并不是 inject 组件的祖先(跨了兄弟分支)。 修法:把 key 抽成共享常量统一引用;确认调用发生在 setup 顶层同步执行;必要时给 inject 传默认值兜底。

  7. emit 事件名对不上,父组件回调不触发 现象:子组件点了按钮,父组件绑定的 @get-message 没反应,还伴随 “Component emitted event ... but it is neither declared” 警告。 原因:defineEmits 里声明的是 camelCase,模板里却写成 kebab-case 之外的拼写;或事件名根本没声明。 修法:defineEmits(['getMessage']) 与 emit('getMessage', data) 严格一致,模板监听写 @get-message 或 @getMessage 均可,但字符串必须对得上。

练习 ​

  1. 生命周期执行顺序:在一个组件中先后注册三次 onMounted(分别打印 1、3、2 的顺序打乱写),再注册一个 onUnmounted,配合 v-if 切换该组件,记录完整输出顺序,并解释“为什么卸载钩子不能叫 onDestroyed”。

  2. 父子双向通信:父组件维护一个 count(ref),子组件有一个输入框和一个按钮。要求子组件不直接修改父组件的 count,而是通过 emit 把增量回传,由父组件完成累加;同时父组件要把 count 实时显示在子组件内部。

  3. 模板引用 + 暴露:封装一个 SearchBox 子组件,内部有 <input> 和 focus() 方法。父组件点击“聚焦搜索框”按钮时让输入框获得焦点。要求父组件不能直接操作子组件 DOM,且子组件不暴露多余成员。

  4. 跨层通信:三层组件 A → B → C。A 持有 theme(ref)和 toggleTheme() 方法,C 需要读取主题并触发切换,B 不做任何数据中转。写出 A、C 两侧代码,并说明如果 C 里想直接 theme.value = 'dark' 会遇到什么问题。

  5. 综合改造:把表格案例中的“删除”功能改为先弹确认框再删除,删除成功后刷新列表,并保证删除过程中按钮处于 loading 状态。指出这段逻辑应该分别放在组合式 API 的哪些位置,以及为什么不能在 setup 顶层直接调用删除接口。

原视频对照 ​

#视频时长
1组合式API-生命周期函数07:16
2组合式API下的父子通信-父传子09:28
3组合式API下的父子通信-子传父07:22
4组合式API-模版引用08:48
5组合式API-provide和inject15:21
6Vue3综合小案例14:37
7补充作业-编辑功能作业14:11

小结 ​

  1. Vue3 组合式 API 的迁移主线是“能合并的合并、能省的省”:beforeCreate/created 被 setup 吸收,destroyed 改名为 unmounted,其余钩子统一加 on 前缀——记住这条命名规则比逐个背表更可靠。
  2. 生命周期钩子是注册式而非覆盖式,同一钩子可注册多次并按注册顺序执行;这既是“不敢动别人代码时追加逻辑”的实用技巧,也意味着排查问题时要注意多段逻辑的叠加影响。
  3. defineProps、defineEmits、defineExpose 属于编译期宏而非运行时函数,它们不需要 import、不能随意赋值、只能在顶层调用——把它们当“给编译器的标注”而不是“API 调用”,很多报错自然就说得通了。
  4. 父子通信的模型在 Vue3 中没有变,变的只是写法:属性下行、事件上行依然是铁律;defineExpose 的引入强化了组件封装边界,默认封闭、按需开放。
  5. provide/inject 解决的是跨层问题,不是“替代 props”。选择通信方式时先看层级:直接父子用 props/emit,跨多层才考虑 provide/inject,避免为了省事滥用导致数据流向难以追踪。
  6. 响应性在跨层传递时不会自动继承:传值传的是快照,传 ref/reactive 才传的是引用。凡是“改了不生效”的问题,先问自己传的到底是值还是容器。
  7. 单向数据流是底线而非限制:底层组件想改顶层数据,正确姿势是拿到修改方法而不是拿到修改权限——这既保证了数据来源唯一,也让调试时的变更入口可追溯。
  8. 从选项式到组合式,业务思路基本不变(声明响应数据 → 调接口 → 赋值 → 绑模板),变的只是代码组织方式;迁移时先照着原思路翻译,再考虑抽成可复用的组合函数。