Skip to content

组合式 API(一) ​

本页对应 组合式 API,共 6 段视频,约 59 分钟。

学习目标 ​

  • 能够对比说明 Vue3 中选项式 API 与组合式 API 的代码组织差异,并指出两者开发思想一致。
  • 能够使用 <script setup> 语法糖声明响应式数据和方法,说明其减少 export default、return 及组件注册样板代码的作用。
  • 能够使用 watch 监听 ref 数据变化,并在回调中正确获取新值与旧值。
  • 能够为 watch 正确配置 immediate 和 deep,并说明深度监听的开销及明确监听对象属性时的函数写法。
  • 能够按初始化、更新、销毁三个阶段回顾生命周期函数,并说明 Vue3 组合式 API 中销毁钩子的名称变化。
  • 能够使用 onMounted 等组合式生命周期函数注册回调,并解释多次调用时的执行顺序及回调执行时机。

知识点 ​

本节回答“为什么要学 Vue3”。笔记只给了框架层面和市场层面两条主线;转写稿主要是 Vue3 基础复习,因此下文把转写稿中的 setup、ref、组合式 API 等演示作为“落地例证”,不额外编造数据。转写稿口述里的“C大/加拿大函数”应纠正为 setup,“热函数/rap”应纠正为 ref,“ra/race”应纠正为 this。

先明确:Vue3 不是推翻 Vue2 ​

为什么需要:转写稿开头强调,Vue3 既支持 Vue2 的选项式 API,也支持组合式 API,开发思想并没有变。学 Vue3 不是把旧知识作废,而是换一套更适合复杂逻辑的组织方式。

怎么想:先承认两套 API 并存,再把 Vue3 的新能力看成“解决 Vue2 痛点的工具”,而不是单纯记新语法。

怎么做:学习时以组合式 API 为主线,同时能读懂选项式 API;遇到旧项目不慌,遇到新项目优先用 Vue3 写法。

易错点:不要把“组合式 API”理解成“只能写组合式”;也不要把 setup 中的 this 当成组件实例——转写稿演示过,setup 里打印 this 是 undefined,不能依赖它。

响应式底层换成 Proxy:数组下标和动态属性更自然 ​

为什么需要:笔记明确写到,Vue3 响应式底层 API 变化为 Proxy,能处理“数组下标的修改”和“对象动态添加属性”。Vue2 时代这些场景容易踩坑,需要额外 API 或特殊写法。

怎么想:Proxy 代理的是整个对象,拦截面更广;开发者应把注意力放在“数据是不是响应式对象”上,而不是记一堆补丁写法。

怎么做:用 reactive 生成对象/数组响应式数据,用 ref 生成简单类型或复杂类型响应式数据。转写稿演示了 ref 返回对象里有固定 value 属性,修改时要写 count.value;但在模板中 ref 会自动解包,不用写 .value。数组修改正常调用数组方法即可。

易错点:reactive 只支持对象/数组,简单类型要用 ref;watch 第一个参数要传 ref 对象,不能写成 count.value,否则传进去的是值,监听不到变化。转写稿里专门提醒了这一点。

TypeScript 支持增强:为类型推导和工程化铺路 ​

为什么需要:笔记指出 Vue2 对 TS 支持弱,Vue3 增强了对 TS 的支持。项目越大,类型提示越能减少拼写错误和接口误用。

怎么想:TS 不是装饰,而是让编辑器在模板和组合式函数里给出更准确的提示。转写稿用对象示例说明:定义好对象后,在模板里输入属性时能看到 name 等提示,这就是类型/编辑器能力带来的开发体验。

怎么做:新项目优先用 TS;写组合式 API 时给 ref、reactive、函数参数补类型;借助 <script setup> 获得更好的推导。

易错点:不要以为用了 Vue3 就自动类型安全,关键数据仍要标注;也不要因为 Vue2 能写 TS 就忽略两者支持程度的差异。

组合式 API:把同一逻辑收拢到一起 ​

为什么需要:选项式 API 按 data、methods、computed 分块,同一业务逻辑被拆散;组合式 API 增强逻辑组合能力,把相关代码放到一起维护。

怎么想:以“功能”为单位组织代码,而不是以“选项类型”为单位。转写稿演示了普通 setup 中变量必须 return 模板才能用;加上 <script setup> 后,不用 export default、不用 return,模板直接使用顶层变量和方法。

怎么做:用 <script setup> 写顶层响应式数据、函数、watch、生命周期。转写稿的演示代码可整理为:

vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
const addCount = () => { count.value++ }
</script>

<template>
  <button @click="addCount">{{ count }}</button>
</template>

调试过程是:删除 export default 和 return 后,模板直接绑定 count,点击按钮自增正常;这说明语法糖减少了样板代码。

易错点:<script setup> 是编译语法糖,浏览器实际运行的仍是编译后的 setup;不要以为它改变了运行原理。普通 setup 中模板要用的变量仍必须 return;<script setup> 中则直接暴露顶层绑定。转写稿还提醒,onMounted 这类组合式函数可以多次调用,回调按调用顺序执行,不要把“注册函数本身”误认为“等渲染完才执行”。

工程化工具更新:create-vue 和 Pinia ​

为什么需要:笔记写到配套工程化工具更新:Vue-cli → create-vue,Vuex → Pinia。新项目如果继续照旧教程装旧工具,容易和 Vue3 生态脱节。

怎么想:框架升级不只是 API 升级,脚手架和状态管理也会跟着换。学习时要把工具链当成 Vue3 的一部分。

怎么做:新项目按 Vue3 生态选择 create-vue 创建项目,状态管理优先学 Pinia;旧项目迁移时再评估 Vuex 存量代码。具体命令以官方文档为准。

易错点:不要把 Vuex 和 Pinia 混着用;不要只背 API 而忽略工程化工具,否则项目初始化就会走回旧路。

市场层面:Vue3 是主战场,Vue2 最新到 2.7 ​

为什么需要:笔记写“市场在扩大,Vue2 最新到 2.7”。这说明 Vue3 生态仍在扩展,而 Vue2 的版本号已经走到 2.7;新项目选型应优先看 Vue3。

怎么想:学技术要看生态和长期维护成本。笔记没有给具体市场份额数字,这里也不补数据;只按笔记结论理解:Vue3 是主要学习方向,Vue2 代码要能读懂。

怎么做:以 Vue3 + 组合式 API + create-vue + Pinia 为主线学习;遇到 Vue2 项目,能识别选项式 API 和旧工具链即可。

易错点:不要把 Vue2.7 当成 Vue3;也不要用旧版教程作为新项目起点。市场扩大不等于可以跳过基础,转写稿里的 ref、watch、生命周期仍是高频内容。

create-vue:为什么需要它 ​

为什么需要: 本节笔记只给了一个核心信息:create-vue 是 Vue 官方新的脚手架工具,底层切换到了 Vite(下一代前端工具链),为开发提供极速响应。也就是说,我们不再把“创建项目结构”和“开发服务器/构建工具”混在一起理解:create-vue 负责按官方推荐方式生成 Vue 项目,Vite 负责项目启动和开发构建。

create-vue 创建项目示意图

怎么想: 先分清“脚手架”和“工具链”。脚手架解决的是“项目从哪来、目录和基础配置怎么生成”;Vite 解决的是“开发时为什么快、构建时怎么处理”。转写稿里反复强调,项目刚克隆下来时默认就是组合式 API + <script setup> 的写法,这说明用 create-vue 生成的项目,后续写代码要按 Vue3 组合式 API 的思路来。

怎么做: 用 create-vue 创建项目时,按终端提示选择即可;具体交互选项以官方文档和终端提示为准,不要死记。项目创建后,先确认它底层由 Vite 驱动,再进入源码写组件。

易错点: 不要把 create-vue 和 Vite 当成同一个东西;也不要编造命令和选项。笔记只说明“官方新脚手架、底层 Vite”,具体命令应看官方文档。

生成项目后先看 setup 语法糖 ​

为什么需要: 转写稿里提到,普通 setup 写法要在模板中使用变量时必须 return,还要写 export default,样板代码多。<script setup> 的目的就是减少样板代码,提升开发体验。

怎么想: 它是编译时语法糖:我们写的是简写,最终仍会被编译成组件 setup 函数的内容。所以顶层声明的变量、函数、导入的组件,可以直接在模板里用,不需要再 return。

怎么做: 在 <script setup> 中声明响应式数据和修改方法,模板直接绑定:

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
const addCount = () => {
  count.value++
}
</script>

<template>
  <button @click="addCount">{{ count }}</button>
</template>

调试时转写稿演示得很清楚:加上 setup 后,原来 export default 会提示不需要,删掉;模板里直接写 count 和 addCount,点击加号功能正常。

易错点: <script setup> 里的代码仍会编译进 setup 函数,组件每次创建时执行;setup 里 this 不可用,打印是 undefined,它不指向当前组件实例;组合式 API 下不要写 onBeforeCreate、onCreated,这两个阶段由 setup 替代。

watch 的调试思路 ​

为什么需要: 当响应式数据变化后要执行一段逻辑,就用 watch。转写稿强调它和 Vue2 的表现一致,只是语法变成函数式。

怎么想: 第一个参数传要监听的 ref 对象本身,不能加 .value;第二个参数是回调,可拿到新值、老值;第三个参数是对象,可放 immediate、deep。

怎么做:

js
import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newValue, oldValue) => {
  console.log('count发生变化', newValue, oldValue)
})

setTimeout(() => {
  count.value = 100
}, 3000)

调试过程:不写 immediate 时,刷新后 3 秒内不打印,3 秒后 count 变 100 才打印;加上 immediate: true 后,一上来先执行一次,3 秒后再执行;监听嵌套对象时,默认深层属性修改不触发,开启 deep: true 才触发。若明确只监听某个深层属性,可把第一个参数写成回调并 return 那个属性,避免无谓的深度递归。

易错点: watch(count.value) 传进去的是值,不是响应式对象;deep 对大对象会递归处理,有性能开销,不要滥用;immediate 要放在第三个参数对象里。

生命周期函数的写法与调试 ​

为什么需要: 组件从创建到销毁有不同阶段,某些逻辑必须在特定阶段执行,例如挂载后初始化图表、卸载前清理定时器和高频事件。

怎么想: 组合式 API 下,生命周期函数以 on 开头,调用时传入回调。注意:onMounted 这个函数本身会立即执行注册,真正等待挂载完成的是传入的回调。

怎么做:

js
import { onMounted } from 'vue'

onMounted(() => {
  console.log('组件挂载完毕')
})

转写稿调试过:写两个 onMounted,不会覆盖,会按调用顺序依次执行。卸载阶段可做清理,Vue3 中销毁相关名称改为 beforeUnmount/unmounted,组合式 API 对应 onBeforeUnmount/onUnmounted。

易错点: 不要写成 onBeforeCreate、onCreated;不要以为 onMounted() 本身等挂载后才执行;keep-alive 下额外有 activated、deactivated,了解即可。

关键文件与本节复习入口 ​

本节关键文件

本节先通过上图定位关键文件,再复习 Vue3 组合式 API 的核心写法。转写稿中反复强调:Vue3 同时支持选项式 API 和组合式 API,开发思想没有变,但组合式 API 更强调“把相同逻辑的代码放在一起维护”。后续新代码统一开启 <script setup>,旧写法只用于对比。

setup 语法糖:少写样板代码 ​

为什么需要:普通 <script setup> 之前,模板要用变量必须先 return,还要写 export default,样板代码多。官方因此提供语法糖,减少开发负担。

怎么想:语法糖不是新运行机制,而是编译阶段的简化写法。写的是简洁代码,编译后仍会变成组件 setup 函数内容。

怎么做:在 script 标签上加 setup,顶层声明的变量、函数、导入内容可直接在模板使用,不需要 return,也不需要 export default。

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
const addCount = () => {
  count.value++
}
</script>

<template>
  <button @click="addCount">{{ count }}</button>
</template>

调试时点击加号,计数正常自增;模板中 ref 会自动解包,所以写 &#123;&#123; count &#125;&#125;,不写 &#123;&#123; count.value &#125;&#125;。

易错点:setup 语法糖下不要继续写 export default;模板要用的顶层变量不要漏声明;组件引入后可直接使用,不需要再注册;不要把“语法糖”理解成浏览器直接运行的新语法。

watch 基础:监听响应式数据变化 ​

为什么需要:当某个响应式数据变化后要执行逻辑,例如打印新值、发请求、做副作用,就需要 watch。它不像计算属性要求纯计算,回调里可以写任意逻辑。

怎么想:watch 接收两个核心参数:监听谁、变化后做什么。第一个参数传 ref 对象本身,不能传 .value,否则就变成普通值了。

怎么做:

js
import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newVal, oldVal) => {
  console.log('count变化', newVal, oldVal)
})

setTimeout(() => {
  count.value = 100
}, 3000)

调试表现:页面初始不打印;3 秒后 count 变为 100,回调执行,能拿到新值 100、老值 0。

易错点:第一个参数不要写 count.value;修改 ref 数据时要写 count.value = 100;回调的新值、老值顺序不要写反。

watch 额外参数:immediate 与 deep ​

为什么需要:有时希望初始化先执行一次回调,或监听嵌套很深的对象属性变化,就需要额外参数。

怎么想:第三个参数是配置对象,不是第二个回调里的参数。immediate 控制立即执行,deep 控制深度侦听。

怎么做:

js
watch(count, (newVal, oldVal) => {
  console.log('count变化', newVal, oldVal)
}, {
  immediate: true
})

开启 immediate 后,页面一上来先打印一次,3 秒后数据变化再打印一次。

深度侦听示例:

js
import { reactive, watch } from 'vue'

const userInfo = reactive({
  info: { name: 'cd' }
})

watch(userInfo, () => {
  console.log('name属性发生变化')
}, { deep: true })

setTimeout(() => {
  userInfo.info.name = '新值'
}, 3000)

调试对比:不写 deep 时,修改 userInfo.info.name 不会触发回调;加上 deep: true 后才会打印。

如果明确只监听某个深层属性,可用回调形式,避免无脑 deep:

js
watch(() => userInfo.info.name, () => {
  console.log('name属性发生变化')
})

易错点:immediate、deep 要放在第三个参数对象里;deep 会递归处理对象,对象很大时有性能开销,不要滥用;明确属性时用回调返回,不要写成普通值。

生命周期函数:组合式 API 下的写法与变化 ​

为什么需要:组件从创建、挂载、更新到销毁,各阶段可能需要发请求、初始化 DOM、清理定时器或高频事件。

怎么想:组合式 API 下生命周期函数以 on 开头,传入回调;没有 onBeforeCreate、onCreated,由 setup 替代。销毁钩子改名为 onBeforeUnmount、onUnmounted。

怎么做:

js
import { onMounted, onBeforeUnmount } from 'vue'

onMounted(() => {
  console.log('组件挂载完毕')
})

onBeforeUnmount(() => {
  clearInterval(timerId)
})

onMounted 可调用多次,回调按注册顺序执行。注意:onMounted 函数本身会立即执行,是“注册”回调;真正等待挂载完成的是传入的回调。

易错点:不要写 onBeforeCreate、onCreated;销毁阶段不要沿用旧名;清理定时器、滚动监听等副作用要放在销毁前钩子中;keep-alive 场景才会额外出现 activated、deactivated,普通组件没有。

setup 选项为什么需要 ​

Vue3 同时支持选项式 API 和组合式 API。选项式 API 的特点是每个东西都有固定位置:数据放一处、方法放一处;组合式 API 则强调把相同逻辑相关的代码放到一起维护。setup 就是组合式 API 的入口,其他组合式函数都要从它开始写。

怎么想:先明确本节讲的是“选项式写法里的 setup 选项”,不是后面才讲的 <script setup> 语法糖。它的作用不是替代生命周期钩子,而是提供一个编写组合式逻辑的入口。

怎么做:在 export default 对象中写 setup(){},需要给模板使用的变量或方法,要以对象形式 return 出去。

易错点:不要以为写了 setup 就自动把内部变量暴露给模板;模板要用,必须 return。也不要把 setup 当成普通生命周期钩子来理解,它执行得非常靠前。

setup 选项的基本写法 ​

本节笔记给出的写法是:

vue
<script>
  export default {
    setup(){
      
    },
    beforeCreate(){
      
    }
  }
</script>

为什么需要:组合式 API 需要一个统一入口,setup 就是这个入口。把相关逻辑写在 setup 内部,后续的组合式函数才有地方组织。

怎么想:可以把它理解成“组件逻辑的起点”。在 setup 里声明响应式数据、方法等;如果模板中要绑定,就把它们放进一个对象里返回。

怎么做:先写 setup(){},再在内部组织逻辑;需要暴露给模板的内容通过 return {} 返回。

易错点:setup 内部不能依赖 this 访问当前组件实例。转写稿中明确提到,在 setup 里打印 this 是 undefined,所以不要写 this.xxx。另外,模板中要用的变量和方法,不管是普通变量还是回调函数,都要 return。

setup 的执行时机 ​

笔记强调:

在 beforeCreate 钩子之前执行

setup与beforeCreate执行顺序示意图

为什么需要:理解执行时机,才能知道哪些事情能放在 setup 里做。因为它非常靠前,所以不能想当然地认为此时已经能拿到组件实例或依赖实例的 this。

怎么想:结合官方生命周期图示记忆:setup 在 beforeCreate 之前执行。也就是说,组件初始化阶段一开始,setup 就先运行,然后才走到 beforeCreate。

怎么做:可以在 setup 和 beforeCreate 中分别打印日志来验证顺序。例如:

vue
<script>
export default {
  setup(){
    console.log('setup')
    console.log(this)
  },
  beforeCreate(){
    console.log('beforeCreate')
  }
}
</script>

按转写稿的验证思路,控制台会先输出 setup,再输出 beforeCreate;同时 setup 中的 this 为 undefined。

易错点:不要误以为 setup 在 beforeCreate 之后执行;也不要在 setup 中通过 this 取组件实例。还有一点:setup 返回的对象才会成为模板可用的绑定内容,未返回的内容模板拿不到。

调试与记忆要点 ​

为什么需要调试:执行时机和 this 问题只看文字容易混,实际打印一次最清楚。

怎么想:把 setup 记成“组合式 API 的入口 + 执行很早 + 没有 this + 模板使用要 return”。

怎么做:保留上面的验证代码运行一次,观察输出顺序和 this 的值;再把需要在模板中使用的变量放进 return {} 中测试。

易错点:不要把本节 setup 选项和 <script setup> 语法糖混为一谈;本节重点是选项式写法中的 setup 及其执行时机。

这一节要解决什么问题 ​

前面我们已经知道,Vue 3 的组合式 API 以 setup 函数作为入口,响应式数据、方法等都可以写在 setup 里面。但这里有一个非常关键的限制:setup 内部声明的变量和方法,默认不会自动暴露给模板。如果模板里想使用它们,必须在 setup 函数末尾以对象的方式 return 出去。

本节笔记里的代码就是在说明这个特点:

vue
<script>
  export default {
    setup(){
      const message = 'this is message'
      const logMessage = ()=>{
        console.log(message)
      }
      // 必须return才可以
      return {
        message,
        logMessage
      }
    }
  }
</script>

也就是说,message 和 logMessage 虽然已经在 setup 里定义好了,但模板并不能直接“看见”它们。只有把它们放进 return 的对象里,模板才能访问。

为什么必须 return ​

转写稿里反复强调了一句话:只要模板中要绑定,就必须 return;不管是一般的变量,还是回调函数,都得 return。 这是因为在普通 setup 写法中,setup 的返回值会作为模板可用的上下文。你可以把 setup 理解成一个“准备数据和方法”的地方,而 return 出来的对象,才是交给模板使用的那一部分。

所以下面这种写法是不完整的:

js
setup(){
  const message = 'this is message'
  const logMessage = ()=>{
    console.log(message)
  }
}

模板里如果写 &#123;&#123; message &#125;&#125;,或者想点击按钮调用 logMessage,都拿不到。不是数据没定义,而是没有暴露出去。

怎么写:数据和方法的暴露 ​

做法很直接:在 setup 末尾写 return,把模板需要的变量和方法放进一个对象里。

js
setup(){
  const message = 'this is message'
  const logMessage = ()=>{
    console.log(message)
  }

  return {
    message,
    logMessage
  }
}

这里 message 是数据,logMessage 是方法。模板里需要哪个,就把哪个放进 return 对象。转写稿里还特别提醒:不要只 return 数据,忘了 return 方法。比如只写了:

js
return {
  message
}

那么模板里使用 message 没问题,但调用 logMessage 就会失败。反过来,如果只 return 方法而没 return 数据,模板里也拿不到 message。

模板如何使用 ​

return 之后,模板里就可以直接使用这些名字。比如:

vue
<template>
  <div>{{ message }}</div>
  <button @click="logMessage">打印</button>
</template>

模板中的 message 和 logMessage,对应的是 setup 返回对象里的同名属性。也就是说,return 对象相当于给模板提供了一份“可用清单”。

易错点与调试 ​

第一个易错点是忘记 return。如果页面里变量不显示、方法点击没反应,先检查 setup 末尾有没有 return,以及 return 对象里有没有对应的名字。

第二个易错点是名字写错。setup 里叫 message,return 里写成 msg,模板里又写 message,就会对不上。模板使用的是 return 对象里的键名。

第三个易错点是在 setup 里使用 this。转写稿里提到,setup 中的 this 是 undefined,它不指向当前组件实例。因此不要想着用 this.message 去访问 setup 内部变量,而应该直接使用局部变量,并通过 return 暴露给模板。

第四个易错点是把本节写法和后面的 <script setup> 语法糖混淆。本节讲的是普通 setup 函数写法,所以必须 return。后面学习 <script setup> 语法糖后,顶层变量和方法可以自动暴露给模板,不需要再写 return。但在当前这一节,必须老老实实 return。

调试时可以用一个简单思路排查:先在 setup 里 console.log(message),确认数据本身存在;再检查 return 对象;最后检查模板绑定名是否一致。这样就能快速定位问题。

一、为什么需要 <script setup> ​

前面用组合式 API 写组件时,常见写法是:在 setup() 里声明响应式数据和方法,最后再把模板要用的内容 return 出去;同时还要保留 export default {} 这类组件选项。代码能跑,但样板内容多,开发体验不够直接。转写稿里的推导很明确:先写普通 setup,发现“模板想用就必须 return”,还要写 export default,于是引出官方提供的 <script setup> 语法糖。它的目标不是改变 Vue 的能力,而是减少开发时的样板代码,让组合式 API 写起来更简洁。这里的“语法糖”要理解成:写法更简单,但编译后仍然会变成浏览器能运行的原本语法。

二、开启方式:给 script 标签加 setup ​

怎么想:语法糖要先“开启”,开启后同一套组合式 API 写法才享受简化。
怎么做:在单文件组件的 <script> 标签上添加 setup 标记即可。
易错点:不要把 setup 写成普通 setup() 函数再混用;开启后原来那套 export default、return 的写法就不需要了。转写稿中加上标记后,编辑器立刻提示原来的 export default 不再需要,把它删掉即可。

vue
<script setup>
  const message = 'this is message'
  const logMessage = ()=>{
    console.log(message)
  }
</script>

三、不再写 export default {},也不再 return ​

为什么需要:普通 setup() 要把模板用到的变量、函数逐个 return,数据一多就很繁琐。
怎么想:<script setup> 里的顶层绑定会被直接暴露给模板,所以不需要再手工组装返回对象。
怎么做:直接声明变量、函数,模板中直接使用。转写稿演示了 count 和 addCount:原来需要 return,开启后直接绑定到模板。
易错点:不要以为“不用 return”等于“变量自动变成响应式”。响应式仍然要靠 ref、reactive 等 API;<script setup> 只负责暴露顶层绑定。

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
const addCount = () => {
  count.value++
}
</script>

<template>
  <div>{{ count }}</div>
  <button @click="addCount">加一</button>
</template>

调试过程也印证了这一点:刷新页面看到初始值 0,点击按钮后 count 正常自增,说明模板已经能直接访问顶层变量和方法。

四、顶层绑定和 import 导入内容都可直接在模板使用 ​

怎么想:既然顶层绑定会暴露给模板,那么顶层声明的变量、函数,以及 import 进来的内容,都可以在模板中直接使用。
怎么做:把模板需要的内容放在 <script setup> 顶层即可。
易错点:注意“顶层”这个范围。写在普通函数内部、条件块内部的局部变量,不会被自动暴露。转写稿还提到 TS 提示会更友好,例如对象属性在模板中能有补全提示,这属于开发体验增强;但不要把它误解成运行时能力变化。

五、组件无需注册,导入后直接使用 ​

为什么需要:以前局部组件通常要“先 import,再在 components 选项里注册,最后模板使用”,步骤多。
怎么想:<script setup> 已经把顶层导入暴露给模板,组件自然也可以导入后直接用。
怎么做:import 组件后,在模板中写对应标签即可,不再写 components 注册项。
易错点:组件名和导入名要对应;不要一边用语法糖,一边又去写旧的 components 注册,造成理解混乱。转写稿明确说这是实际开发中会百分百用到的特性。

六、语法糖的本质:编译后仍是 setup ​

转写稿中打开官方在线编译示例:左边是开发者写的 <script setup>,右边编译结果里仍然能看到熟悉的 setup 对象结构。这说明语法糖不是“新造一套运行时”,而是编译阶段把简化写法转换成原本支持的 setup 形式。怎么想:看到简化写法,也要知道它最终会落到组件 setup 函数内容中。怎么做:开发时放心用简洁写法,理解上不要丢掉“编译”这一层。易错点:不要把 <script setup> 当成普通函数调用;也不要误以为它内部代码在浏览器里原样运行。官方文档还提到更少样板、更好的 TS 推导、性能优化等优势,但本节重点先掌握写法变化。

七、小结与易错点清单 ​

本节核心就三句话:script 标签加 setup;不再写 export default {}、不再手工 return;顶层绑定和导入组件可直接在模板使用。调试时按“加标记→删旧模板代码→声明数据→刷新看初始值→点击验证方法”的顺序走,就能确认语法糖生效。后续写组合式 API 时,转写稿也建议统一开启 <script setup>,忘掉旧写法,减少心智负担。

代码示例 ​

示例一:<script setup> 语法糖 vs 普通 setup 写法 ​

普通写法(App.vue,笔记 1、2 节):

vue
<script>
import { ref } from 'vue'
export default {
  setup() {
    const message = 'this is message'
    const count = ref(0)
    const addCount = () => {
      count.value++
    }
    // 必须 return 才可以给模板使用
    return {
      message,
      count,
      addCount
    }
  }
}
</script>

<template>
  <div>{{ message }} - {{ count }}</div>
  <button @click="addCount">加一</button>
</template>

语法糖写法(App.vue,在 create-vue 工程中运行):

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
const addCount = () => {
  count.value++
}
</script>

<template>
  <button @click="addCount">{{ count }}</button>
</template>

关键行与易错点:

  • 只要在 script 标签上加 setup 标记:不用写 export default {},不用 return,顶层声明的变量、函数、import 进来的内容都直接在模板可用。
  • 语法糖是"编译期"的糖:它会被编译成 setup() 函数里 return 的形式,浏览器里实际运行的还是老语法,所以开发体验更好,但本质没变。
  • 组件同样免注册:import Child from './Child.vue' 之后直接在模板里写 <Child />,不用再写 components 选项。
  • 易错点:ref 数据在 script 里必须 count.value,在模板里会自动解套,模板里不要写 &#123;&#123; count.value &#125;&#125;。

示例二:watch 基础用法 + immediate / deep / 函数式监听器 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>watch 使用</title>
</head>
<body>
  <div id="app">
    <p>count:{{ count }}</p>
    <p>user.info.name:{{ user.info.name }}</p>
  </div>

  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <script>
    const { createApp, ref, watch, onMounted } = Vue

    createApp({
      setup() {
        const count = ref(0)
        const user = ref({ info: { name: 'Tom' } })

        // 1. 基础用法:第一个参数传 ref 对象本身
        watch(count, (newVal, oldVal) => {
          console.log('count 变化了', newVal, oldVal)
        })

        // 2. immediate:一上来先执行一次
        watch(count, (newVal) => {
          console.log('immediate 立刻执行一次', newVal)
        }, { immediate: true })

        // 3. deep:默认浅监听,深层属性改不到,需要开启 deep
        watch(user, () => {
          console.log('user 深层属性发生变化了')
        }, { deep: true })

        // 4. 明确知道监听哪个属性时,用函数式监听器,不用 deep
        watch(() => user.value.info.name, (newVal, oldVal) => {
          console.log('name 变化了', newVal, oldVal)
        })

        onMounted(() => {
          setTimeout(() => { count.value = 100 }, 3000)
          setTimeout(() => { user.value.info.name = 'Jerry' }, 6000)
        })

        return { count, user }
      }
    }).mount('#app')
  </script>
</body>
</html>

关键行与易错点:

  • 第一个参数不能加 .value:watch(count, ...) 传的是 ref 对象;写成 watch(count.value, ...) 就变成一个普通值 0,监听不到任何变化。
  • 第三个参数是配置对象:{ immediate: true }、{ deep: true },别和第二个回调参数的位置写混。
  • 回调可以拿新值和老值:(newVal, oldVal) => {}。
  • deep 的代价:会对传入对象做递归处理,对象很大时耗时明显,所以要少用。明确知道监听哪个属性时,优先用 () => obj.value.info.name 这种函数式写法(函数必须有 return,不能直接写 obj.value.info.name,否则又变成值了)。
  • watch 回调里可以随便写副作用(发请求、改 DOM 都行),这一点和计算属性"只能纯计算"不同。

示例三:组合式 API 生命周期函数 + 多次调用 + 清理副作用 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>生命周期函数</title>
</head>
<body>
  <div id="app">
    <button @click="visible = !visible">切换子组件</button>
    <child v-if="visible"></child>
  </div>

  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <script>
    const { createApp, ref, onMounted, onUnmounted } = Vue

    const Child = {
      template: `<div>我是子组件,count:{{ count }}</div>`,
      setup() {
        const count = ref(0)
        let timerId = null

        // 同一个钩子可以被调用多次,按调用顺序依次执行回调
        onMounted(() => {
          console.log('组件挂载完毕 1')
        })
        onMounted(() => {
          console.log('组件挂载完毕 2')
        })

        timerId = setInterval(() => {
          count.value++
        }, 1000)

        // 销毁时清理定时器,避免内存泄漏
        onUnmounted(() => {
          clearInterval(timerId)
          console.log('组件销毁,定时器已清除')
        })

        return { count }
      }
    }

    createApp({
      components: { Child },
      setup() {
        const visible = ref(true)
        return { visible }
      }
    }).mount('#app')
  </script>
</body>
</html>

关键行与易错点:

  • 组合式 API 下钩子都是 on + 钩子名 的函数:onMounted、onUnmounted,必须传一个回调,回调里写逻辑。
  • 名称有变化:Vue2 的 beforeDestroy / destroyed 在 Vue3 变成 onBeforeUnmount / onUnmounted。
  • 组合式 API 里没有 onBeforeCreate 和 onCreated,这两个阶段直接被 setup 取代。
  • 最容易理解错的一点:onMounted(fn) 这个调用本身是立即执行的(相当于往内部数组里 push 一个回调),真正等到挂载完成才执行的是你传进去的那个 fn。
  • 同一个钩子可以调用多次,回调按注册顺序依次执行(先 1 后 2),不是覆盖。
  • onUnmounted 里做副作用清理:清定时器、解绑 window 上的滚动等高频事件,否则组件销毁后定时器还在跑,属于内存泄漏。

常见错误 ​

  1. 现象:修改数据后 watch 回调不触发。原因:第一个参数写成 count.value,传进去的是值不是 ref 对象。修法:传 count 本身;要监听对象具体属性时用 () => obj.name。
  2. 现象:脚本中改了 ref,页面或后续逻辑不更新。原因:脚本里忘记通过 .value 读写。修法:脚本中统一 .value,模板中不写 .value。
  3. 现象:<script setup> 中编译报错或出现冗余代码。原因:仍保留 export default {}、setup()、return {}。修法:删除这些模板代码,顶层声明直接在模板使用。
  4. 现象:卸载清理逻辑不执行。原因:生命周期仍写 beforeDestroy/destroyed,或写了不存在的 onBeforeCreate/onCreated。修法:改用 onBeforeUnmount/onUnmounted;创建前逻辑放 setup。
  5. 现象:模板里组件不识别。原因:<script setup> 中未 import,或还在 components 里注册。修法:先导入组件,删除注册步骤,直接模板使用。
  6. 现象:大对象监听后性能明显变差。原因:无脑加 deep: true,导致深层递归监听。修法:明确属性用 getter;只有属性不确定或多层多属性变化时才用 deep。
  7. 现象:setup 中访问 this 得到 undefined。原因:组合式 API 的 setup 不指向组件实例。修法:不要用 this,直接使用导入的 API 和顶层变量/函数。

练习 ​

  1. 用 <script setup> 实现计数器:count 用 ref,提供加一方法,模板绑定显示和点击;全程不写 export default、setup()、return。
  2. 定义 firstName、lastName 两个 ref,用 computed 生成 fullName;在计算函数中打印一次,并在模板连续使用三次,观察缓存表现。
  3. 实现搜索关键词监听:keyword 用 ref,watch 加 immediate,打印新值和旧值;再对一个嵌套表单对象分别用“getter 监听具体字段”和“deep 监听整个对象”两种写法做对比。
  4. 组件挂载时启动 setInterval,卸载时清理定时器;要求使用 onMounted、onBeforeUnmount,并说明不清理会带来什么问题。
  5. 把一段选项式 API 代码改写为 <script setup>:包含 data、方法、computed、watch、mounted、beforeDestroy 六类内容。

原视频对照 ​

#视频时长
1Vue3基础语法复习07:38
2组合式API-setup语法糖12:14
3组合式API-watch基础使用08:00
4组合式API-watch的额外参数10:37
5组合式API-生命周期函数回顾10:55
6组合式API-生命周期函数使用10:04

小结 ​

  1. 新代码统一优先 <script setup>,减少样板代码。
  2. 响应式数据优先用 ref,降低心智负担;脚本中记得 .value,模板中自动解包。
  3. watch 第一个参数传响应式对象本身;监听具体属性用 getter,不要误传值。
  4. deep 不是默认选项,明确字段优先 getter,大对象谨慎开启。
  5. 组合式生命周期统一 onXxx 命名;没有 onBeforeCreate/onCreated,卸载钩子名称已变化。
  6. <script setup> 中组件导入即可用,不需要再注册。
  7. 定时器、高频事件等副作用要在卸载钩子中清理。
  8. 面试回答生命周期按“初始化、更新、销毁”分阶段,再补业务中实际用过的钩子和场景。