一、Vue 3 与组合式 API
Vue 3 兼容 Vue 2 中的大部分模板和选项式 API,但新项目通常推荐使用组合式 API。选项式 API 以 data、methods、computed 等配置项组织代码;组合式 API 则在 setup() 中按功能组合响应式数据和方法,更适合大型组件和逻辑复用。
两种风格可以在 Vue 3 中共存,但不建议在同一个组件里反复混用。组合式 API 中没有组件实例 this,响应式变量和方法需要显式返回(普通 setup)或由 <script setup> 自动暴露。
二、Vue 3 快速体验
1. 组合式 API
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Vue 3 组合式 API</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<p>计数:{{ count }}</p>
<button @click="add">加一</button>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const count = ref(0)
function add() {
count.value++
}
// setup 返回的内容才能在模板中使用。
return { count, add }
}
}).mount('#app')
</script>
</body>
</html>
ref(0) 返回一个响应式引用对象。在 JavaScript 中需要通过 .value 读写,在模板中 Vue 会自动解包,因此直接写 {``{ count }} 即可。
2. 选项式 API
js
import { createApp } from 'vue'
createApp({
data() {
return { count: 100 }
},
methods: {
add() {
this.count++
}
}
}).mount('#app')
选项式 API 对 Vue 2 开发者比较熟悉,Vue 3 仍然支持。新代码可以根据团队规范选择一种风格,但要保持同一个组件结构清晰。
三、使用 Vite 创建 Vue 3 项目
Vite 是现代前端构建工具。开发环境下它可以按需提供模块并快速启动,修改代码后通过 HMR(热模块替换)更新页面。生产构建时仍会对代码进行打包和优化。
Vite 需要较新的 Node.js 版本,创建项目前应查看当前模板的版本要求:
bash
node -v
npm -v
npm config set registry https://registry.npmmirror.com
npm create vite@latest vue3-demo
cd vue3-demo
npm install
npm run dev
常用脚本来自 package.json:
json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
npm run dev 用于开发,npm run build 生成部署文件,npm run preview 用于本地预览构建结果。Vite 项目常见目录包括 src/main.js、src/App.vue、src/components 和 public;业务代码主要放在 src 中。
四、setup、ref 与 reactive
普通 setup() 是组合式 API 的入口。它执行时机接近 Vue 2 的 beforeCreate 和 created,不能在其中依赖已经挂载的 DOM。普通写法必须返回模板需要的变量和方法。
ref 适合包装数字、字符串、布尔值,也可以包装对象和数组;reactive 适合直接代理对象或数组。ref 在 JavaScript 中通过 .value 操作,reactive 直接读写属性。
vue
<template>
<p>数字:{{ count }}</p>
<button @click="increase">增加</button>
<p>姓名:{{ user.name }},年龄:{{ user.age }}</p>
<button @click="user.age++">增加年龄</button>
<p v-for="girl in girls" :key="girl">{{ girl }}</p>
<button @click="changeFirstGirl">修改第一项</button>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const user = reactive({ name: 'lqz', age: 19 })
const girls = reactive(['小王', '小李'])
function increase() {
count.value++
}
function changeFirstGirl() {
girls[0] = '新名字'
}
return { count, user, girls, increase, changeFirstGirl }
}
}
</script>
推荐使用 const 保存 ref 或 reactive 返回的对象引用。const 不允许重新绑定变量,但不影响修改响应式对象内部的数据。不要把 ref 变量整体重新赋成普通值,否则会丢失响应式引用。
五、计算属性 computed
计算属性适合表示由其他响应式数据推导出的值。它会根据依赖缓存结果,依赖不变时不会重复计算;与每次渲染都执行的方法相比,计算属性更适合过滤、拼接和统计。
vue
<template>
<input v-model="person.firstName" placeholder="姓">
<input v-model="person.lastName" placeholder="名">
<p>姓名:{{ fullName }}</p>
<input v-model="fullName" placeholder="修改完整姓名">
</template>
<script>
import { reactive, computed } from 'vue'
export default {
setup() {
const person = reactive({ firstName: '', lastName: '' })
const fullName = computed({
get() {
return person.firstName + person.lastName
},
set(value) {
person.firstName = value.slice(0, 1)
person.lastName = value.slice(1)
}
})
return { person, fullName }
}
}
</script>
只读计算属性可以直接传入函数;需要通过 v-model 修改时,应提供 get 和 set。不要在计算属性中执行发送请求、修改其他状态等副作用。
六、watch 与 watchEffect
watch 需要明确指定观察源,适合监听一个或多个值变化后执行请求、日志或数据同步。监听 reactive 对象的某个属性时,通常传入返回该属性的函数。
js
import { ref, reactive, watch } from 'vue'
const height = ref(180)
watch(height, (newValue, oldValue) => {
console.log('身高变化:', oldValue, newValue)
})
const person = reactive({ name: 'lqz', age: 19 })
watch(() => person.name, (newValue, oldValue) => {
console.log('姓名变化:', oldValue, newValue)
})
const sum = ref(0)
const message = ref('正常')
watch([sum, message], ([newSum, newMessage]) => {
console.log(newSum, newMessage)
})
watchEffect 不需要手动指定依赖,它会执行回调并自动收集回调中访问的响应式数据。依赖变化时会重新执行,适合依赖较多但关系直接的副作用。
js
import { ref, watchEffect } from 'vue'
const keyword = ref('')
const page = ref(1)
watchEffect(() => {
console.log(`请求关键词 ${keyword.value} 的第 ${page.value} 页`)
})
需要停止监听时,watch 和 watchEffect 都会返回停止函数。组件卸载时 Vue 会自动停止在组件中创建的监听,但手动创建的异步任务仍需自行处理。
七、Vue 3 生命周期
Vue 3 选项式 API 的生命周期名称与 Vue 2 基本一致,但销毁阶段改为 beforeUnmount 和 unmounted。组合式 API 使用对应的 onXxx 函数注册钩子。
| Vue 2/选项式 | 组合式 API | 常见用途 |
|---|---|---|
created |
setup() |
初始化数据、准备请求 |
beforeMount |
onBeforeMount |
挂载前 |
mounted |
onMounted |
DOM 已挂载 |
beforeUpdate |
onBeforeUpdate |
DOM 更新前 |
updated |
onUpdated |
DOM 更新后 |
beforeDestroy |
onBeforeUnmount |
卸载前清理 |
destroyed |
onUnmounted |
卸载完成 |
vue
<script>
import {
ref,
onMounted,
onBeforeUnmount
} from 'vue'
export default {
setup() {
const count = ref(0)
let timerId
onMounted(() => {
timerId = setInterval(() => {
count.value++
}, 1000)
})
onBeforeUnmount(() => {
clearInterval(timerId)
})
return { count }
}
}
</script>
定时器、WebSocket、全局事件监听和订阅都应在卸载前清理,否则页面离开后仍可能继续执行,造成内存泄漏或重复请求。
八、在生命周期中请求后端
需要在组件创建时加载数据,可以在 setup() 中直接调用请求,也可以在 onMounted 中调用。页面需要 DOM 时应选择 onMounted;只依赖数据时放在 setup 或 watchEffect 中即可。
vue
<template>
<p v-if="loading">加载中...</p>
<p v-else-if="error">{{ error }}</p>
<p v-else>姓名:{{ user.name }},性别:{{ user.gender }}</p>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
const user = ref({ name: '', gender: '' })
const loading = ref(true)
const error = ref('')
onMounted(async () => {
try {
const response = await axios.get('http://127.0.0.1:5000/info')
user.value = response.data
} catch {
error.value = '用户信息加载失败'
} finally {
loading.value = false
}
})
</script>
接口失败时要给用户明确反馈,并在真实项目中配置 CORS、超时和统一请求错误处理。
九、<script setup> 语法
<script setup> 是 Vue 3 单文件组件的编译期语法糖。里面声明的变量、函数和导入的组件会自动暴露给模板,不需要再写 setup() 和 return,也不需要手动注册导入的组件。
vue
<script setup>
import { ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
const name = ref('lqz')
const message = '父组件传给子组件的消息'
function changeName() {
name.value = '彭于晏'
}
</script>
<template>
<h1>{{ name }}</h1>
<button @click="changeName">修改名字</button>
<HelloWorld :msg="message" />
</template>
导入组件时必须写出正确的 .vue 文件路径。使用 <script setup lang="ts"> 时,可以进一步为 props、事件和响应式变量添加 TypeScript 类型。
十、script setup 中的组件通信
1. 父传子:defineProps
vue
<!-- Child.vue -->
<script setup>
const props = defineProps({
msg: { type: String, required: true }
})
</script>
<template>
<p>收到的消息:{{ props.msg }}</p>
</template>
模板中也可以直接使用 msg,但保留 props 对象有助于明确数据来源。defineProps 是编译器宏,不需要从 Vue 导入。
2. 子传父:defineEmits
vue
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import ChildEditor from './ChildEditor.vue'
const message = ref('')
function receive(value) {
message.value = value
}
</script>
<template>
<p>子组件消息:{{ message }}</p>
<ChildEditor @send="receive" />
</template>
vue
<!-- ChildEditor.vue -->
<script setup>
import { ref } from 'vue'
const emit = defineEmits(['send'])
const text = ref('')
function send() {
emit('send', text.value)
}
</script>
<template>
<input v-model="text">
<button @click="send">发送</button>
</template>
3. script setup 中使用 ref
模板 ref 需要先声明一个同名的 ref()。在组件挂载后,普通标签的值是 DOM 对象,组件的值是组件实例。
vue
<script setup>
import { ref, onMounted } from 'vue'
import ChildPanel from './ChildPanel.vue'
const inputRef = ref(null)
const childRef = ref(null)
onMounted(() => {
inputRef.value.focus()
childRef.value.changeTitle('父组件设置的标题')
})
</script>
<template>
<input ref="inputRef">
<ChildPanel ref="childRef" />
</template>
子组件必须使用 defineExpose 明确公开给父组件访问的内容:
vue
<script setup>
import { ref } from 'vue'
const title = ref('子组件标题')
function changeTitle(value) {
title.value = value
}
defineExpose({ title, changeTitle })
</script>
<template><h2>{{ title }}</h2></template>
十一、script setup 中的插槽
Vue 3 推荐使用 v-slot 或 # 语法。父组件传入具名模板,子组件在对应的 <slot> 位置渲染。
vue
<!-- HelloWorld.vue -->
<template>
<slot name="header"></slot>
<h2>公共组件标题</h2>
<slot name="footer"></slot>
</template>
vue
<!-- App.vue -->
<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<HelloWorld>
<template #header><h3>父组件标题</h3></template>
<template #footer><button>父组件按钮</button></template>
</HelloWorld>
</template>
十二、总结
- Vue 3 组合式 API 通过
ref、reactive、computed、watch和生命周期函数组织逻辑。 ref适合值类型,reactive适合对象和数组;在 JavaScript 中注意.value的使用。- 计算属性用于派生值,
watch和watchEffect用于副作用;不要把三者混为一谈。 <script setup>会自动暴露变量和组件,父子通信使用defineProps、defineEmits和defineExpose。- 定时器、请求和 DOM 操作要结合生命周期管理,并在组件卸载时清理资源。