Vue3 的组合式 API 更适合把同一功能的变量、计算属性、监听器和方法放在一起,避免 Vue2 选项式 API 中 data、computed、methods 按类型分散导致的维护成本。原文以搜索和排序为例说明:Vue2 要把搜索数据、排序数据分别塞进 data,把计算逻辑塞进 computed,把方法塞进 methods;Vue3 则可以把搜索模块和排序模块物理隔离,代码复用性更强。本文整理其中可直接落地的组合式 API 核心语法与响应式实践,路由、Pinia 等部分原文未给出完整代码,暂不展开。
一、环境与项目入口
先配置 Node/npm 环境,确认版本后在指定目录创建 Vue 项目,执行 npm install 安装依赖,再用本地编辑器打开。项目入口 index.html 中保留 id 为 app 的挂载点,并通过 module 脚本引入 src/main.ts。main.ts 的核心逻辑是引入 createApp 和根组件 App.vue,然后挂载到 #app。
- import { createApp } from 'vue'
- import App from './App.vue'
- createApp(App).mount('#app')
复制代码
App.vue 由 template、script、style 三部分组成,分别承载组件结构、逻辑和样式。原文示例在 script 中导出组件名称,在 style 中演示了颜色、背景、阴影、圆角和内边距等基础样式。
二、setup 与 script setup
setup 是 Vue3 新增配置项,值是一个函数。它会在组件实例化阶段之前执行,因此 setup 不能直接访问选项式 API 中的 data、methods 等数据;反过来,选项式 API 可以访问 setup 返回的数据。原文特别指出,setup 执行早于选项式数据初始化,这是两者访问方向不同的原因。
常规写法是在 export default 中声明 setup,把 ref 创建的响应式数据和修改方法返回出去。更常用的是 script setup 语法糖,在 script 标签上声明 setup 后,顶层变量和函数可直接在模板中使用,不需要显式 return。
- import { ref } from 'vue'
- const message = ref('Hello Vue 3!')
- const changeMessage = () => {
- message.value = 'You clicked the button!'
- }
复制代码
三、ref 与 reactive 的选择
ref 用于创建响应式数据,适合数字、字符串、布尔值等原始类型,也可以通过 .value 访问对象、数组、函数等任意类型的真实值;在模板中会自动解包。reactive 用于声明对象响应式,适合把一组相关字段组织成对象。原文建议当前主流做法是二者并用,但以 ref 为主、reactive 为辅。
- import { ref, reactive, toRefs } from 'vue'
- type PersonInfo = {
- name: string
- age: number
- tel: string
- }
- const personRef = ref<PersonInfo>({
- name: '超哥',
- age: 18,
- tel: '13800138000'
- })
- const person = reactive<PersonInfo>({
- name: '超哥',
- age: 18,
- tel: '13800138000'
- })
- const { name, age, tel } = toRefs(person)
复制代码
reactive 对象不能直接整体替换,需要借助 Object.assign 合并,否则会丢失响应式引用。toRefs 的作用是把 reactive 对象的每个属性转换成仍然保持响应式的 ref,从而可以安全解构;它只能作用于 reactive,不能用于普通对象,也不能直接用于 ref。toRef 则用于单独取某一个属性,例如 toRef(state, 'name')。
四、computed 的派生状态与缓存
computed 适合从已有响应式状态推导新值,具备派生状态、自动依赖追踪和缓存三个特征。以 count 推导 doubleCount 为例,只要 count.value 没变化,多次访问 doubleCount.value 都不会重新执行计算函数;count.value 改变后才会重新计算。这与普通函数每次调用都执行不同。
原文还演示了可写计算属性:通过 get 把 firstName 和 lastName 组合成 fullName,通过 set 把赋给 fullName 的新值拆回源数据。
- import { ref, computed } from 'vue'
- const firstName = ref('John')
- const lastName = ref('Doe')
- const fullName = computed({
- get() {
- return firstName.value + ' ' + lastName.value
- },
- set(newValue) {
- const names = newValue.split(' ')
- firstName.value = names[0]
- lastName.value = names[names.length - 1]
- }
- })
- fullName.value = 'Jane Smith'
复制代码
五、watch 的监视方式与常见误区
watch 用于监视数据变化,可监视 ref 定义的数据、函数返回的值、reactive 定义的数据,以及包含上述内容的数组。实际开发中,如果只想拿到某个具体属性的新旧值,优先写成 getter 函数,例如 () => person.value.age,这样性能更好,也能拿到准确 oldVal。
- import { ref, watch } from 'vue'
- const person = ref({ name: '超哥', age: 18 })
- const stopWatch = watch(
- () => person.value.age,
- (newAge, oldAge) => {
- console.log(`年龄从 ${oldAge} 变成了 ${newAge}`)
- if (newAge >= 40) {
- stopWatch()
- }
- }
- )
复制代码
如果直接 watch 一个 ref 对象,默认是浅监视,修改对象内部属性不会触发;需要手动开启 { deep: true }。但即使开启深度监视,newVal 和 oldVal 仍然是同一个对象引用,所以 newVal === oldVal 为 true。原文用房子地址作比喻:监视的是地址,内部装修变了,地址没变,回调里读到的已经是修改后的值。
reactive 对象的监视相当于自动开启深度监视,修改第一层属性或深层嵌套属性都能触发,不需要额外加 deep。监视多个属性时,把 getter 放进数组,回调参数也按数组解构。
- watch(
- [() => person.value.name, () => person.value.age],
- ([newName, newAge], [oldName, oldAge]) => {
- console.log([[newName, newAge], [oldName, oldAge]])
- }
- )
复制代码
watch 会返回一个停止监听的函数,调用它即可取消监听。原文示例在年龄达到 40 时执行 stopWatch(),停止后续回调。
六、watchEffect 的自动依赖追踪
watchEffect 会立即执行一次传入函数,并在执行过程中自动追踪用到的响应式依赖,依赖变化后重新执行。它不需要显式告诉 Vue 监视谁,适合逻辑中自然读取多个响应式变量的场景。
- import { ref, watchEffect } from 'vue'
- const water = ref(20)
- const pressure = ref(100)
- watchEffect(() => {
- if (water.value > 60 || pressure.value > 200) {
- console.log('警告:锅炉异常!')
- console.log(`当前状态:水温${water.value},水压${pressure.value}`)
- }
- })
复制代码
七、标签 ref、Props 与生命周期
标签 ref 用于引用 DOM 元素或组件实例。在 script setup 中创建 ref 并与模板中的 ref 绑定后,可以通过 .value 获取 DOM 引用,例如读取元素尺寸或输出 DOM 信息。
Props 是组件对外暴露的输入接口,用于父组件向子组件传递数据,体现 Vue 的单向数据流:数据拥有者负责修改,其他组件通过约定方式请求修改。子组件可用 defineProps 接收并约束类型。
- const x = defineProps<{ a: string }>()
- console.log(x)
复制代码
生命周期方面,script setup 本身相当于创建阶段。挂载阶段常用 onBeforeMount 和 onMounted,DOM 操作和请求通常放在 onMounted;更新阶段有 onBeforeUpdate 和 onUpdated;销毁阶段有 onBeforeUnmount 和 onUnmounted,适合清除定时器、解绑事件监听。父子组件顺序可记为:加载渲染时父先开始、子先结束;更新时父先发现、子先更新;销毁时父先动手、子先消失。
八、Hooks 的抽取时机
Hooks 是把某个功能的变量和方法单独定义在 ts 文件中,需要时直接导入,避免多个组件重复声明同一套逻辑。原文建议,当发现两个组件写了类似代码时,就是创建 Hooks 的最佳时机。由于原文该部分代码展示不完整,这里只保留其设计思路:先按功能聚合逻辑,再抽离为可复用模块,最后在组件中导入调用。 |