这份手册涵盖 Vue3 从入门到进阶的核心知识点,兼顾新手友好性和实战实用性,以 组合式 API(Composition API) 为核心(Vue3 推荐用法),配套完整示例和关键说明。
安装 Node.js(推荐 v16.x 及以上版本),自带 npm/yarn 包管理器:
- 全局安装 Vue CLI
- 创建项目
- 选择配置:
- 上下箭头选择
Default (Vue 3)(默认 Vue3 配置)或 Manually select features(自定义配置)
- 回车等待项目创建完成
- 启动项目
cd vue3-demo
npm run serve
- 创建 Vite + Vue3 项目
npm create vite@latest vue3-vite-demo --template vue
npm create vite@latest vue3-vite-demo -- --template vue
yarn create vite vue3-vite-demo --template vue
- 安装依赖并启动
cd vue3-vite-demo
npm install
npm run dev
vue3-demo/
├── node_modules/ # 项目依赖包
├── public/ # 静态资源(不会被打包处理)
├── src/ # 核心源码目录(重点关注)
│ ├── assets/ # 静态资源(会被打包处理,如图片、样式)
│ ├── components/ # 公共组件
│ ├── App.vue # 根组件
│ ├── main.js # 入口文件
│ └── style.css # 全局样式
├── package.json # 项目配置(依赖、脚本命令)
└── vite.config.js # Vite 配置文件(Vue CLI 对应 vue.config.js)
Vue3 单文件组件由 3 个部分组成,是 Vue 项目的基础单位:
<!-- src/components/HelloWorld.vue -->
<template>
<!-- 模板区域:HTML 结构,支持 Vue 模板语法 -->
<div class="hello">
<h1>{{ msg }}</h1>
<button @click="changeMsg">修改消息</button>
</div>
</template>
<script setup>
// 脚本区域:Vue3 推荐使用 <script setup> 语法糖(简化组合式 API 写法)
// 无需手动导出,变量/方法直接在模板中使用
// 1. 定义响应式变量(ref 用于基本类型)
import { ref } from 'vue'
const msg = ref('Hello Vue3!')
// 2. 定义方法
const changeMsg = () => {
msg.value = 'Vue3 组合式 API 真好用!'
// 注意:ref 定义的变量,在脚本中需通过 .value 访问/修改,模板中无需 .value
}
</script>
<style scoped>
/* 样式区域:scoped 表示样式仅作用于当前组件,避免样式污染 */
.hello {
text-align: center;
margin-top: 50px;
}
button {
padding: 8px 16px;
cursor: pointer;
}
</style>
Vue3 提供两种核心响应式 API:ref 和 reactive,用于创建响应式数据(数据变化时,视图自动更新)。
- 适用场景:数字、字符串、布尔值等基本类型,也可用于引用类型(对象、数组)
- 核心特性:脚本中通过
.value 访问 / 修改,模板中直接使用
- 示例:
<script setup>
import { ref } from 'vue'
// 基本类型响应式数据
const count = ref(0)
const username = ref('张三')
const isShow = ref(true)
// 引用类型响应式数据
const user = ref({
age: 20,
gender: '男'
})
const list = ref([1, 2, 3])
// 修改数据
const updateData = () => {
count.value++
username.value = '李四'
user.value.age = 21 // 引用类型同样需要 .value 访问内部属性
list.value.push(4)
}
</script>
<template>
<div>{{ count }}</div>
<div>{{ username }}</div>
<div v-if="isShow">{{ user.age }}</div>
<div>{{ list }}</div>
<button @click="updateData">修改数据</button>
</template>
- 适用场景:对象、数组等引用类型(不支持基本类型)
- 核心特性:无需
.value 访问 / 修改,直接操作属性;返回的是原始对象的代理对象
- 示例:
<script setup>
import { reactive } from 'vue'
// 对象类型响应式数据
const user = reactive({
name: '王五',
info: {
height: 180,
hobby: ['篮球', '游戏']
}
})
// 数组类型响应式数据
const todoList = reactive([
{ id: 1, title: '学习 Vue3', done: false },
{ id: 2, title: '练习代码', done: true }
])
// 修改数据
const updateReactiveData = () => {
user.name = '赵六'
user.info.height = 185
user.info.hobby.push('读书')
todoList[0].done = true
todoList.push({ id: 3, title: '掌握响应式 API', done: false })
}
</script>
<template>
<div>{{ user.name }} - {{ user.info.height }}</div>
<div>{{ user.info.hobby }}</div>
<ul>
<li v-for="item in todoList" :key="item.id">
{{ item.title }} - {{ item.done ? '已完成' : '未完成' }}
</li>
</ul>
<button @click="updateReactiveData">修改响应式数据</button>
</template>
toRefs:将 reactive 创建的代理对象,转换为 ref 格式的对象,支持解构赋值不丢失响应式
<script setup>
import { reactive, toRefs } from 'vue'
const user = reactive({
name: '张三',
age: 20
})
// 解构后仍保持响应式
const { name, age } = toRefs(user)
const update = () => {
name.value = '李四' // 需 .value(因为已经是 ref 类型)
age.value = 21
}
</script>
toRef:为 reactive 对象的单个属性创建 ref 引用,适用于只需要单个属性的场景
<script setup>
import { reactive, toRef } from 'vue'
const user = reactive({
name: '张三',
age: 20
})
const nameRef = toRef(user, 'name')
const updateName = () => {
nameRef.value = '李四'
}
</script>
- 作用:根据已有数据推导新数据,缓存计算结果(依赖不变时,不会重复计算)
- 示例:
<script setup>
import { ref, computed } from 'vue'
const score = ref(85)
// 只读计算属性
const scoreLevel = computed(() => {
if (score.value >= 90) return '优秀'
else if (score.value >= 80) return '良好'
else if (score.value >= 60) return '及格'
else return '不及格'
})
// 可读写计算属性(需传入一个包含 get 和 set 的对象)
const fullName = ref('')
const firstName = ref('张')
const lastName = ref('三')
const fullNameComputed = computed({
// 获取值
get() {
return `${firstName.value}${lastName.value}`
},
// 设置值
set(val) {
firstName.value = val[0]
lastName.value = val.slice(1)
}
})
// 修改可读写计算属性
const setFullName = () => {
fullNameComputed.value = '李四'
}
</script>
<template>
<div>分数:{{ score }}</div>
<div>评级:{{ scoreLevel }}</div>
<button @click="score++">加分</button>
<div>全名:{{ fullNameComputed }}</div>
<button @click="setFullName">修改全名</button>
</template>
- 作用:监听一个或多个响应式数据的变化,执行副作用操作(如接口请求、日志打印等)
- 核心特性:惰性执行(默认只有数据变化时才执行)、可获取新旧值
- 示例:
<script setup>
import { ref, reactive, watch } from 'vue'
// 1. 监听单个 ref 数据
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// 2. 监听多个响应式数据(传入数组)
const name = ref('张三')
const age = ref(20)
watch([name, age], ([newName, newAge], [oldName, oldAge]) => {
console.log(`姓名:${oldName} -> ${newName}`)
console.log(`年龄:${oldAge} -> ${newAge}`)
})
// 3. 监听 reactive 对象(默认深度监听)
const user = reactive({
info: {
height: 180
}
})
watch(user, (newVal) => {
console.log('user 发生变化:', newVal.info.height)
})
// 4. 监听 reactive 对象的单个属性(需传入函数返回值)
watch(() => user.info.height, (newVal) => {
console.log(`身高变为 ${newVal}`)
})
// 5. 配置项(immediate:立即执行;deep:深度监听)
const obj = ref({
a: {
b: 1
}
})
watch(obj, (newVal) => {
console.log('obj.a.b 变化:', newVal.a.b)
}, {
immediate: true, // 组件挂载时立即执行一次
deep: true // 深度监听 ref 引用类型的内部属性
})
// 修改数据触发 watch
const updateData = () => {
count.value++
name.value = '李四'
age.value = 21
user.info.height = 185
obj.value.a.b = 2
}
</script>
<template>
<button @click="updateData">修改所有数据</button>
</template>
- 作用:自动监听回调函数中使用的响应式数据,无需手动指定监听目标,简化代码
- 核心特性:立即执行(组件挂载时执行一次)、自动收集依赖
- 示例:
<script setup>
import { ref, watchEffect } from 'vue'
const keyword = ref('')
const list = ref([])
// 自动收集 keyword 作为依赖,keyword 变化时重新执行
watchEffect(() => {
console.log(`搜索关键词:${keyword.value}`)
// 模拟接口请求:根据关键词获取列表
if (keyword.value) {
list.value = [`${keyword.value}-1`, `${keyword.value}-2`, `${keyword.value}-3`]
} else {
list.value = []
}
})
// 停止监听(如需手动停止)
// const stopWatch = watchEffect(() => { ... })
// stopWatch() // 调用即可停止监听
</script>
<template>
<input v-model="keyword" placeholder="请输入搜索关键词" />
<ul>
<li v-for="item in list" :key="item">{{ item }}</li>
</ul>
</template>
- 语法:
{{ 表达式 }}
- 支持:变量、算术运算、三元表达式、对象属性访问等
- 示例:
<template>
<div>{{ username }}</div>
<div>{{ count + 1 }}</div>
<div>{{ isShow ? '显示' : '隐藏' }}</div>
<div>{{ user.info.age }}</div>
</template>
指令是 Vue 提供的特殊 HTML 特性,以 v- 开头,用于操作 DOM。
v-bind:绑定 HTML 属性(缩写::)
<template>
<!-- 绑定单个属性 -->
<div :id="boxId" :class="boxClass">绑定样式类</div>
<!-- 绑定对象形式的 class -->
<div :class="{ active: isActive, 'text-red': isRed }">动态class</div>
<!-- 绑定对象形式的 style -->
<div :style="{ color: textColor, fontSize: '16px' }">动态style</div>
<!-- 绑定组件 props(后续组件通信会详细讲) -->
<Child :msg="parentMsg" />
</template>
v-on:绑定事件(缩写:@)
<template>
<!-- 绑定基本事件 -->
<button @click="handleClick">点击事件</button>
<!-- 传递参数 -->
<button @click="handleParams(123, $event)">传递参数($event 是原生事件对象)</button>
<!-- 事件修饰符:.stop(阻止冒泡)、.prevent(阻止默认行为)等 -->
<a @click.prevent="handleLink" href="https://vuejs.org">阻止默认跳转</a>
<!-- 按键修饰符:.enter(回车触发)等 -->
<input @keyup.enter="handleEnter" placeholder="回车触发" />
</template>
v-if / v-else-if / v-else:条件渲染(动态创建 / 销毁 DOM)
<template>
<div v-if="score >= 90">优秀</div>
<div v-else-if="score >= 80">良好</div>
<div v-else-if="score >= 60">及格</div>
<div v-else>不及格</div>
</template>
v-show:条件显示(切换 DOM 的 display 属性,不销毁 DOM)
<template>
<div v-show="isShow">v-show 显示/隐藏</div>
</template>
v-if vs v-show:频繁切换用 v-show(性能更好),首次渲染或切换不频繁用 v-if(节省 DOM 资源)
v-for:列表渲染
<template>
<!-- 遍历数组:item 是当前项,index 是索引(可选) -->
<ul>
<li v-for="(item, index) in todoList" :key="item.id">
{{ index + 1 }}. {{ item.title }}
</li>
</ul>
<!-- 遍历对象:value 是值,key 是键,index 是索引(可选) -->
<div v-for="(value, key, index) in user" :key="key">
{{ index }}. {{ key }}: {{ value }}
</div>
</template>
- 必传
:key:用于 Vue 识别列表项的唯一性,优化列表渲染性能(推荐使用唯一 ID,不推荐使用 index)
v-model:双向数据绑定(简化表单交互)
<template>
<!-- 输入框 -->
<input v-model="username" placeholder="请输入用户名" />
<!-- 复选框 -->
<input type="checkbox" v-model="isAgree" /> 同意协议
<!-- 单选框 -->
<input type="radio" v-model="gender" value="male" /> 男
<input type="radio" v-model="gender" value="female" /> 女
<!-- 下拉框 -->
<select v-model="city">
<option value="">请选择城市</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
</select>
</template>
<script setup>
import { ref } from 'vue'
const username = ref('')
const isAgree = ref(false)
const gender = ref('male')
const city = ref('')
</script>
Vue3 组件分为 局部注册(推荐,仅当前组件可用)和 全局注册(全局可用,适合通用组件)。
直接在使用组件的父组件中导入并使用(无需额外注册,<script setup> 中自动局部注册)
<!-- 父组件:Parent.vue -->
<template>
<div>
<h2>父组件</h2>
<!-- 直接使用子组件 -->
<Child :msg="parentMsg" />
</div>
</template>
<script setup>
// 导入子组件(自动局部注册)
import Child from './Child.vue'
import { ref } from 'vue'
const parentMsg = ref('来自父组件的消息')
</script>
<!-- 子组件:Child.vue -->
<template>
<div class="child">
<h3>子组件</h3>
<div>{{ msg }}</div>
</div>
</template>
<script setup>
// 定义 props(接收父组件传递的数据)
const props = defineProps({
msg: {
type: String,
required: true, // 是否必传
default: '' // 默认值(非必传时生效)
}
})
</script>
在入口文件 main.js 中注册,全局所有组件均可直接使用
import { createApp } from 'vue'
import App from './App.vue'
import GlobalChild from './components/GlobalChild.vue'
const app = createApp(App)
app.component('GlobalChild', GlobalChild)
app.mount('#app')
<!-- 任意组件中直接使用 -->
<template>
<GlobalChild />
</template>
Vue3 组件通信的核心方式如下(按使用频率排序):
- 子组件通过
defineProps 定义接收的属性
- 父组件通过
:prop名 传递数据
- 示例:见「局部注册」中的 Parent.vue 和 Child.vue
- 子组件通过
defineEmits 定义要触发的事件,通过 emit('事件名', 数据) 传递数据
- 父组件通过
@事件名 监听子组件事件并接收数据
<!-- 子组件:Child.vue -->
<template>
<button @click="handleSend">向父组件传递数据</button>
</template>
<script setup>
// 定义要触发的事件
const emit = defineEmits(['childMsg'])
const handleSend = () => {
// 触发事件并传递数据
emit('childMsg', '来自子组件的消息', 123)
}
</script>
<!-- 父组件:Parent.vue -->
<template>
<Child @childMsg="handleChildMsg" />
<div>子组件传递的数据:{{ childData }}</div>
</template>
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const childData = ref('')
// 接收子组件数据
const handleChildMsg = (msg, num) => {
console.log(num) // 123
childData.value = msg
}
</script>
- 父组件通过
provide 提供数据
- 深层子组件通过
inject 注入数据
- 无需逐层传递 props,简化跨层级通信
<!-- 父组件:Parent.vue -->
<template>
<Child />
</template>
<script setup>
import { provide, ref } from 'vue'
import Child from './Child.vue'
// 提供普通数据(非响应式)
provide('staticMsg', '父组件提供的静态消息')
// 提供响应式数据
const parentCount = ref(0)
provide('reactiveCount', parentCount)
// 提供方法(用于子组件修改父组件数据)
provide('updateCount', () => {
parentCount.value++
})
</script>
<!-- 深层子组件:GrandChild.vue -->
<template>
<div>静态消息:{{ staticMsg }}</div>
<div>响应式计数:{{ reactiveCount }}</div>
<button @click="updateCount">修改父组件计数</button>
</template>
<script setup>
import { inject } from 'vue'
// 注入数据
const staticMsg = inject('staticMsg')
const reactiveCount = inject('reactiveCount')
const updateCount = inject('updateCount')
</script>
- 父组件通过
ref 绑定子组件 / DOM,通过 ref.value 访问
- 子组件通过
defineExpose 暴露内部属性 / 方法(<script setup> 中默认私有,需手动暴露)
<!-- 子组件:Child.vue -->
<template>
<div>子组件内部计数:{{ childCount }}</div>
</template>
<script setup>
import { ref } from 'vue'
const childCount = ref(0)
const childMethod = () => {
childCount.value++
}
// 暴露内部属性/方法给父组件
defineExpose({
childCount,
childMethod
})
</script>
<!-- 父组件:Parent.vue -->
<template>
<!-- 绑定子组件 ref -->
<Child ref="childRef" />
<!-- 绑定 DOM ref -->
<div ref="domRef">DOM 元素</div>
<button @click="handleAccessChild">访问子组件</button>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
// 子组件 ref
const childRef = ref(null)
// DOM ref
const domRef = ref(null)
// 组件挂载后才能访问 ref(DOM/子组件已创建)
onMounted(() => {
console.log('DOM 元素:', domRef.value)
})
const handleAccessChild = () => {
// 访问子组件暴露的属性
console.log('子组件计数:', childRef.value.childCount)
// 调用子组件暴露的方法
childRef.value.childMethod()
}
</script>
Vue3 组合式 API 的生命周期钩子与 Options API 对应,需从 vue 中导入后使用,核心钩子如下:
示例:
<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'
const count = ref(0)
let timer = null
// 组件挂载后启动定时器
onMounted(() => {
console.log('组件挂载完成')
timer = setInterval(() => {
count.value++
}, 1000)
})
// 组件更新后触发
onUpdated(() => {
console.log('组件数据更新了')
})
// 组件销毁前清除定时器
onUnmounted(() => {
console.log('组件即将销毁')
clearInterval(timer)
})
</script>
- 作用:将组件的 DOM 结构传送至页面指定位置(如
<body> 末尾),解决层级嵌套导致的样式 / 布局问题(如弹窗、模态框)
- 语法:
<Teleport to="选择器">
- 示例:
<template>
<button @click="isShowModal = true">打开弹窗</button>
<!-- 将弹窗传送至 body 末尾 -->
<Teleport to="body">
<div class="modal" v-if="isShowModal">
<div class="modal-content">
<h3>弹窗内容</h3>
<button @click="isShowModal = false">关闭弹窗</button>
</div>
</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue'
const isShowModal = ref(false)
</script>
<style scoped>
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
}
</style>
- 作用:等待异步组件加载完成前,显示加载状态,提升用户体验
- 语法:
<Suspense> 包含 <template #default>(异步组件)和 <template #fallback>(加载状态)
- 示例:
<!-- 父组件 -->
<template>
<div>
<h2>异步组件加载示例</h2>
<Suspense>
<!-- 异步组件 -->
<template #default>
<AsyncComponent />
</template>
<!-- 加载状态 -->
<template #fallback>
<div>正在加载组件...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
// 动态导入(异步组件)
const AsyncComponent = () => import('./AsyncComponent.vue')
</script>
<!-- 异步组件:AsyncComponent.vue -->
<template>
<div>异步组件加载完成!</div>
</template>
<script setup>
// 模拟异步请求(组件加载时等待 2 秒)
const fetchData = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, 2000)
})
}
// 组件加载时执行异步操作
await fetchData()
</script>
Pinia 是 Vue3 推荐的状态管理库,相比 Vuex 更简洁、更易用,支持 TypeScript 友好。
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
在 src/store 目录下创建 Store 文件(如 userStore.js)
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
username: '张三',
token: '',
userInfo: null
}),
getters: {
upperCaseUsername: (state) => {
return state.username.toUpperCase()
}
},
actions: {
updateUsername(newName) {
this.username = newName
},
async fetchUserInfo() {
const res = await new Promise(resolve => {
setTimeout(() => {
resolve({
id: 1,
name: '李四',
age: 20
})
}, 1000)
})
this.userInfo = res
this.token = 'mock-token-123456'
}
}
})
在组件中导入并使用 Store
<template>
<div>
<div>用户名:{{ userStore.username }}</div>
<div>大写用户名:{{ userStore.upperCaseUsername }}</div>
<div>用户信息:{{ userStore.userInfo }}</div>
<button @click="userStore.updateUsername('王五')">修改用户名</button>
<button @click="userStore.fetchUserInfo()">获取用户信息</button>
<button @click="resetStore">重置 Store</button>
</div>
</template>
<script setup>
// 导入 Store
import { useUserStore } from '@/store/userStore'
// 创建 Store 实例
const userStore = useUserStore()
// 重置 Store(恢复初始状态)
const resetStore = () => {
userStore.$reset()
}
// 也可通过解构获取 state 属性(需用 storeToRefs 保持响应式)
// import { storeToRefs } from 'pinia'
// const { username, upperCaseUsername } = storeToRefs(userStore)
</script>
在 src/router 目录下创建 index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
import Detail from '@/views/Detail.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/detail/:id',
name: 'Detail',
component: Detail,
props: true
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
- 路由出口与导航
<!-- src/App.vue -->
<template>
<div id="app">
<!-- 路由导航(替代 a 标签) -->
<nav>
<router-link to="/">首页</router-link>
<router-link to="/about">关于我们</router-link>
<router-link :to="{ name: 'Detail', params: { id: 1 } }">详情页</router-link>
</nav>
<!-- 路由出口:匹配的组件将渲染在这里 -->
<router-view />
</div>
</template>
- 组件中操作路由
<!-- src/views/Detail.vue -->
<template>
<div>
<h2>详情页</h2>
<div>当前 ID:{{ id }}</div>
<button @click="goBack">返回上一页</button>
<button @click="goHome">返回首页</button>
</div>
</template>
<script setup>
// 1. 通过 props 接收动态路由参数
const props = defineProps({
id: {
type: [String, Number],
required: true
}
})
// 2. 导入路由实例
import { useRouter, useRoute } from 'vue-router'
const router = useRouter() // 路由操作实例(跳转、后退等)
const route = useRoute() // 路由信息实例(获取参数、路径等)
// 打印当前路由参数
console.log('路由参数:', route.params.id)
// 路由跳转
const goHome = () => {
// 方式1:路径跳转
// router.push('/')
// 方式2:命名路由跳转(推荐,无需关心路径)
router.push({ name: 'Home' })
}
// 后退上一页
const goBack = () => {
router.go(-1)
}
</script>
- 路由守卫:控制路由访问权限(如登录拦截)
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.name === 'About' && !token) {
next({ name: 'Home' })
} else {
next()
}
})
整合 Vue3 核心知识点,实现一个简易待办事项功能:
<template>
<div class="todo-container" style="width: 400px; margin: 50px auto;">
<h2>Todo List</h2>
<!-- 添加待办 -->
<div class="add-todo">
<input
v-model="todoTitle"
placeholder="请输入待办事项"
@keyup.enter="addTodo"
style="width: 300px; padding: 8px;"
/>
<button @click="addTodo" style="padding: 8px 16px; margin-left: 8px;">添加</button>
</div>
<!-- 待办列表 -->
<ul class="todo-list" style="margin-top: 20px; padding: 0; list-style: none;">
<li
v-for="item in todoList"
:key="item.id"
style="padding: 10px; border: 1px solid #eee; margin-bottom: 8px; display: flex; justify-content: space-between;"
>
<span :style="{ textDecoration: item.done ? 'line-through' : 'none' }">
{{ item.title }}
</span>
<div>
<button
@click="toggleTodo(item.id)"
style="margin-right: 8px; cursor: pointer;"
>
{{ item.done ? '取消' : '完成' }}
</button>
<button @click="deleteTodo(item.id)" style="cursor: pointer; color: red;">
删除
</button>
</div>
</li>
</ul>
<!-- 统计信息 -->
<div class="todo-stats" style="margin-top: 20px;">
剩余待办:{{ remainingCount }} / 总数量:{{ todoList.length }}
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 待办标题
const todoTitle = ref('')
// 待办列表
const todoList = ref([
{ id: 1, title: '学习 Vue3 基础', done: false },
{ id: 2, title: '掌握组合式 API', done: true }
])
// 计算属性:剩余待办数量
const remainingCount = computed(() => {
return todoList.value.filter(item => !item.done).length
})
// 添加待办
const addTodo = () => {
if (!todoTitle.value.trim()) return
const newTodo = {
id: Date.now(), // 用时间戳作为唯一 ID
title: todoTitle.value.trim(),
done: false
}
todoList.value.push(newTodo)
todoTitle.value = '' // 清空输入框
}
// 切换待办状态
const toggleTodo = (id) => {
const todo = todoList.value.find(item => item.id === id)
if (todo) {
todo.done = !todo.done
}
}
// 删除待办
const deleteTodo = (id) => {
todoList.value = todoList.value.filter(item => item.id !== id)
}
</script>
- Q:ref 数据为什么在脚本中需要 .value,模板中不需要?
A:ref 本质是一个包装对象,.value 是其内部存储值的属性;Vue 在模板编译时,会自动解析 ref 的 .value,因此模板中无需手动书写。
- Q:reactive 解构后丢失响应式怎么办?
A:使用 toRefs 或 toRef 将 reactive 对象转换为 ref 格式,解构后仍保持响应式。
- Q:
<script setup> 中如何暴露组件属性 / 方法给父组件?
A:使用 defineExpose 手动暴露,父组件通过 ref 引用访问。
- Q:Pinia 与 Vuex 的区别?
A:Pinia 无需区分 mutations 和 actions(仅用 actions 支持同步 / 异步),无需嵌套模块,更简洁,天然支持 TypeScript,是 Vue3 官方推荐。
- TypeScript + Vue3(类型安全,生产项目必备)
- Vue3 组件库(Element Plus、Ant Design Vue 等)
- 项目打包优化(Vite 构建优化、懒加载、CDN 加速等)
- 服务端渲染(Nuxt3,基于 Vue3 的 SSR 框架)