Vue实例与数据绑定
如果说Vue是一座大厦,那么Vue实例就是这座大厦的地基。地基打得牢,大厦才能稳。
在上一篇文章中,我们成功搭建了开发环境,并写出了第一个Vue应用。今天,让我们深入理解Vue的核心——Vue实例与数据绑定。
📌 写作约定:本系列文章以 Vue 3
<script setup>语法糖 为主要讲解方式,这是Vue 3.2+官方推荐的写法。同时会顺带介绍Vue 2和Vue 3 Options API的写法作为对比,帮助大家理解演进过程和维护老项目。
一、Vue实例:应用的"大脑"
每个Vue应用都从一个Vue实例开始。你可以把它想象成应用的"大脑",它管理着数据、方法和整个应用的生命周期。
1.1 创建Vue实例
在Vue 3中,创建应用实例的方式:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
1.2 组件的"五脏六腑"
一个完整的Vue组件可以包含以下部分。先看Vue 3 <script setup>语法糖写法(推荐):
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
// =================== 数据:组件的"记忆" ===================
const count = ref(0)
const user = ref({ name: '张三', age: 25 })
const items = ref(['苹果', '香蕉', '橙子'])
// =================== 计算属性:组件的"派生数据" ===================
const doubleCount = computed(() => count.value * 2)
const fullName = computed(() => `${user.value.name}(${user.value.age}岁)`)
// =================== 侦听器:组件的"观察员" ===================
watch(count, (newVal, oldVal) => {
console.log(`count从${oldVal}变成了${newVal}`)
})
// =================== 方法:组件的"行为" ===================
const increment = () => {
count.value++
}
const greet = (name) => {
return `你好,${name}!`
}
// =================== 生命周期钩子 ===================
onMounted(() => {
console.log('DOM挂载完成')
})
</script>
对比Vue 3 Options API写法:
<script>
export default {
data() {
return {
count: 0,
user: { name: '张三', age: 25 },
items: ['苹果', '香蕉', '橙子']
}
},
computed: {
doubleCount() {
return this.count * 2
},
fullName() {
return `${this.user.name}(${this.user.age}岁)`
}
},
watch: {
count(newVal, oldVal) {
console.log(`count从${oldVal}变成了${newVal}`)
}
},
methods: {
increment() {
this.count++
},
greet(name) {
return `你好,${name}!`
}
},
mounted() {
console.log('DOM挂载完成')
}
}
</script>
对比Vue 2写法(已过时,了解即可):
<script>
export default {
data() {
return {
count: 0
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('DOM挂载完成')
}
}
</script>
1.3 三种写法对比总结
| 特性 | Vue 3 <script setup> | Vue 3 Options API | Vue 2 |
|---|---|---|---|
| 代码量 | 最少 | 较多 | 较多 |
| this | 不需要 | 需要 | 需要 |
| 类型推断 | 优秀 | 一般 | 弱 |
| 学习曲线 | 中等 | 低 | 低 |
| 官方推荐 | ✅ 推荐 | 兼容维护 | ❌ 已停止维护 |
1.4 关于this的烦恼
在<script setup>语法糖中,不需要使用this,直接使用响应式变量即可:
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++ // ✅ 直接访问
console.log(count.value)
}
const log = () => {
console.log(count.value)
}
const doBoth = () => {
increment() // ✅ 直接调用
log()
}
</script>
而在Options API中,需要通过this访问:
export default {
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++ // 需要this
this.log() // 需要this
},
log() {
console.log(this.count)
}
}
}
Options API的常见陷阱:箭头函数没有自己的this:
export default {
data() {
return { count: 0 }
},
methods: {
// ❌ 错误:箭头函数的this不指向Vue实例
wrongIncrement: () => {
this.count++ // 报错!
},
// ✅ 正确:普通函数
correctIncrement() {
this.count++
}
}
}
💡
<script setup>的优势:彻底告别this的烦恼,代码更简洁,类型推断更友好。
二、生命周期:Vue实例的"人生旅程"
每个Vue实例都有完整的生命周期——从创建到销毁,就像人的一生。理解生命周期,你就能在正确的时机做正确的事。
2.1 生命周期全景图
┌─────────────────────────────────────────────────────────────┐
│ Vue 3 生命周期 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 创建阶段 │
│ ┌─────────────┐ │
│ │ setup() │ ← <script setup>中的代码直接执行 │
│ └─────────────┘ 相当于 beforeCreate + created │
│ │
│ 挂载阶段 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ onBefore │───▶│ onMounted │ │
│ │ Mount │ │ │ │
│ └─────────────┘ └─────────────┘ │
│ │ │ │
│ │ DOM已挂载 │
│ │ 可访问DOM元素 │
│ │ 适合发起网络请求 │
│ │
│ 更新阶段(数据变化时触发) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ onBefore │───▶│ onUpdated │ │
│ │ Update │ │ │ │
│ └─────────────┘ └─────────────┘ │
│ │ │
│ DOM已更新 │
│ │
│ 卸载阶段 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ onBefore │───▶│ onUnmounted │ │
│ │ Unmount │ │ │ │
│ └─────────────┘ └─────────────┘ │
│ │ │
│ 实例已销毁 │
│ 清理定时器、事件监听器 │
│ │
└─────────────────────────────────────────────────────────────┘
2.2 常用生命周期钩子
Vue 3 <script setup> 写法(推荐):
<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'
const count = ref(0)
let timer = null
// =================== setup阶段:代码直接执行 ===================
// 相当于 created,数据已初始化,可访问响应式数据
console.log('组件创建完成')
// =================== onMounted:DOM已经渲染完成 ===================
onMounted(() => {
console.log('DOM挂载完成,可以访问DOM元素')
timer = setInterval(() => {
console.log('定时器运行中...')
}, 1000)
})
// =================== onUpdated:数据变化导致DOM更新后 ===================
onUpdated(() => {
console.log('DOM更新完成')
})
// =================== onUnmounted:组件已卸载 ===================
onUnmounted(() => {
console.log('组件已卸载')
clearInterval(timer) // 重要:清理定时器
})
</script>
对比Vue 3 Options API写法:
<script>
export default {
data() {
return { count: 0 }
},
created() {
console.log('组件创建完成')
},
mounted() {
console.log('DOM挂载完成')
},
updated() {
console.log('DOM更新完成')
},
beforeUnmount() { // Vue 3改名了
console.log('组件即将卸载')
},
unmounted() { // Vue 3改名了
console.log('组件已卸载')
}
}
</script>
对比Vue 2写法:
<script>
export default {
data() {
return { count: 0 }
},
created() {
console.log('组件创建完成')
},
mounted() {
console.log('DOM挂载完成')
},
beforeDestroy() { // Vue 2叫这个
console.log('组件即将销毁')
},
destroyed() { // Vue 2叫这个
console.log('组件已销毁')
}
}
</script>
2.3 生命周期钩子对照表
<script setup> | Options API (Vue 3) | Options API (Vue 2) | 触发时机 |
|---|---|---|---|
| 代码直接执行 | created | created | 实例创建完成 |
onBeforeMount | beforeMount | beforeMount | DOM挂载前 |
onMounted | mounted | mounted | DOM挂载完成 |
onBeforeUpdate | beforeUpdate | beforeUpdate | 数据变化DOM更新前 |
onUpdated | updated | updated | DOM更新完成 |
onBeforeUnmount | beforeUnmount | beforeDestroy | 实例卸载前 |
onUnmounted | unmounted | destroyed | 实例卸载后 |
2.4 使用场景速查
| 场景 | 推荐钩子 | 示例 |
|---|---|---|
| 发起API请求 | onMounted 或直接执行 | 获取初始数据 |
| 操作DOM | onMounted | 初始化图表库 |
| 设置定时器 | onMounted | 轮询、倒计时 |
| 清理定时器 | onUnmounted | 防止内存泄漏 |
| 监听窗口事件 | onMounted + onUnmounted | resize、scroll |
三、响应式数据:Vue的"魔法"
响应式数据是Vue最核心的特性,它让数据和视图自动保持同步。
3.1 响应式原理简介
Vue 3使用Proxy实现响应式,Vue 2使用Object.defineProperty:
- Vue 2:给对象的每个属性装"监控器",新增属性需要用
Vue.set() - Vue 3:给整个对象请"管家",新增属性自动响应式
3.2 ref vs reactive
Vue 3 <script setup> 写法:
<script setup>
import { ref, reactive } from 'vue'
// =================== ref:万能选择 ===================
const count = ref(0)
const name = ref('张三')
const user = ref({ age: 25 }) // 对象也可以用ref
// 访问和修改需要 .value
console.log(count.value) // 读取
count.value++ // 修改
user.value.age = 26 // 修改对象属性
// =================== reactive:仅用于对象/数组 ===================
const state = reactive({
name: '李四',
age: 25,
hobbies: ['编程', '阅读']
})
// 不需要 .value
console.log(state.name) // 读取
state.age++ // 修改
state.hobbies.push('游戏') // 修改数组
</script>
<template>
<!-- 模板中ref自动解包,不需要.value -->
<p>{{ count }}</p>
<p>{{ state.name }}</p>
</template>
选择建议:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 基本类型 | ref | reactive不支持基本类型 |
| 对象 | ref 或 reactive | 都可以,ref更统一 |
| 需要整体替换 | ref | state.value = newObj |
| 解构需求 | reactive + toRefs | 保持响应性 |
3.3 响应式陷阱与解决
陷阱一:解构丢失响应性
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({
name: '张三',
age: 25
})
// ❌ 错误:解构后失去响应性
const { name, age } = state
// ✅ 正确:使用toRefs保持响应性
const { name, age } = toRefs(state)
</script>
陷阱二:reactive被整体替换
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
// ❌ 错误:整体替换会丢失响应性
const wrongReset = () => {
state = { count: 0 } // state不再是响应式的
}
// ✅ 正确:修改属性
const rightReset = () => {
state.count = 0
}
</script>
陷阱三:ref在模板中的自动解包
<script setup>
import { ref } from 'vue'
const count = ref(0)
const user = ref({ name: '张三' })
</script>
<template>
<!-- ✅ 正确:自动解包 -->
<p>{{ count }}</p>
<p>{{ user.name }}</p>
<!-- ❌ 错误:不需要.value -->
<p>{{ count.value }}</p>
</template>
四、计算属性:数据的"变形金刚"
计算属性根据已有数据派生新数据,只有依赖变化时才重新计算,具有缓存特性。
4.1 基本用法
Vue 3 <script setup> 写法:
<template>
<p>总价:{{ totalPrice }}</p>
<p>双倍:{{ doubleCount }}</p>
</template>
<script setup>
import { ref, computed } from 'vue'
const price = ref(100)
const quantity = ref(2)
const discount = ref(0.8)
const count = ref(5)
// =================== 计算属性:有缓存 ===================
const totalPrice = computed(() => {
console.log('计算属性执行了') // 依赖不变就不会再执行
return price.value * quantity.value * discount.value
})
const doubleCount = computed(() => count.value * 2)
</script>
对比Vue 3 Options API写法:
export default {
data() {
return {
price: 100,
quantity: 2,
discount: 0.8
}
},
computed: {
totalPrice() {
return this.price * this.quantity * this.discount
}
}
}
4.2 计算属性 vs 方法
<template>
<!-- 计算属性:有缓存,多次访问只计算一次 -->
<p>{{ totalPrice }}</p>
<p>{{ totalPrice }}</p>
<!-- 方法:每次调用都执行 -->
<p>{{ getTotalPrice() }}</p>
<p>{{ getTotalPrice() }}</p>
</template>
<script setup>
import { ref, computed } from 'vue'
const price = ref(100)
const totalPrice = computed(() => {
console.log('计算属性执行')
return price.value * 2
})
const getTotalPrice = () => {
console.log('方法执行')
return price.value * 2
}
</script>
4.3 可写计算属性
计算属性默认只读,但也可以设置setter:
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// =================== 可写计算属性 ===================
const fullName = computed({
get() {
return `${firstName.value}${lastName.value}`
},
set(value) {
firstName.value = value.charAt(0)
lastName.value = value.slice(1)
}
})
// 使用setter
const changeName = () => {
fullName.value = '李四' // 自动拆分为 firstName='李', lastName='四'
}
</script>
五、侦听器:数据的"守门员"
侦听器用于在数据变化时执行异步或开销较大的操作。
5.1 基本用法
Vue 3 <script setup> 写法:
<script setup>
import { ref, watch } from 'vue'
const searchKeyword = ref('')
const searchResults = ref([])
// =================== 监听ref ===================
watch(searchKeyword, (newVal, oldVal) => {
console.log(`从 "${oldVal}" 变为 "${newVal}"`)
searchResults.value = []
})
</script>
对比Vue 3 Options API写法:
export default {
data() {
return {
searchKeyword: '',
searchResults: []
}
},
watch: {
searchKeyword(newVal, oldVal) {
console.log(`从 "${oldVal}" 变为 "${newVal}"`)
this.searchResults = []
}
}
}
5.2 监听选项
<script setup>
import { ref, watch } from 'vue'
const searchKeyword = ref('')
watch(searchKeyword, (newVal) => {
console.log('搜索:', newVal)
}, {
immediate: true, // 立即执行一次
deep: false, // 深度监听(用于对象)
flush: 'post' // DOM更新后执行
})
</script>
5.3 监听对象属性
<script setup>
import { ref, reactive, watch } from 'vue'
// =================== 监听ref对象的属性 ===================
const user = ref({
name: '张三',
profile: { age: 25 }
})
// 方式一:getter函数
watch(() => user.value.name, (newVal) => {
console.log('名字变了:', newVal)
})
// 方式二:深度监听整个对象
watch(user, (newVal) => {
console.log('user变了')
}, { deep: true })
// 方式三:监听嵌套属性
watch(() => user.value.profile.age, (newVal) => {
console.log('年龄变了:', newVal)
})
// =================== 监听reactive对象 ===================
const state = reactive({
count: 0,
user: { name: '李四' }
})
// reactive的属性可以直接监听
watch(() => state.count, (newVal) => {
console.log('count变了:', newVal)
})
// 监听整个reactive对象(自动deep)
watch(state, (newVal) => {
console.log('state变了')
})
</script>
5.4 实战:搜索防抖
<template>
<input v-model="keyword" placeholder="搜索..." />
<div v-if="loading">搜索中...</div>
<ul v-else>
<li v-for="item in results" :key="item.id">{{ item.name }}</li>
</ul>
</template>
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
const results = ref([])
const loading = ref(false)
let timer = null
watch(keyword, (newVal) => {
clearTimeout(timer)
timer = setTimeout(async () => {
if (!newVal.trim()) {
results.value = []
return
}
loading.value = true
// 模拟API请求
await new Promise(r => setTimeout(r, 300))
results.value = [
{ id: 1, name: `${newVal}结果1` },
{ id: 2, name: `${newVal}结果2` }
]
loading.value = false
}, 500) // 防抖500ms
})
</script>
5.5 watchEffect:自动追踪依赖
Vue 3还提供了watchEffect,自动追踪回调中使用的响应式数据:
<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('张三')
// 自动追踪:用到谁就监听谁
watchEffect(() => {
console.log(`count=${count.value}, name=${name.value}`)
// count或name变化都会触发
})
</script>
六、计算属性 vs 侦听器:如何选择?
6.1 对比总结
| 特性 | 计算属性 | 侦听器 |
|---|---|---|
| 返回值 | 必须返回 | 可选 |
| 缓存 | ✅ 有 | ❌ 无 |
| 异步 | ❌ 不支持 | ✅ 支持 |
| 适用场景 | 数据派生、格式化 | 异步请求、副作用 |
6.2 选择指南
用计算属性:
- 根据已有数据计算新数据
- 需要缓存避免重复计算
- 纯函数,无副作用
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const list = ref([{ id: 1, active: true }])
// ✅ 适合计算属性
const fullName = computed(() => `${firstName.value}${lastName.value}`)
const activeList = computed(() => list.value.filter(i => i.active))
</script>
用侦听器:
- 需要执行异步操作
- 数据变化时执行副作用
- 需要比较新旧值
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
const userId = ref(1)
// ✅ 适合侦听器:异步请求
watch(keyword, (val) => {
fetchResults(val)
})
// ✅ 适合侦听器:比较新旧值
watch(userId, (newVal, oldVal) => {
if (newVal !== oldVal) {
fetchUser(newVal)
}
})
</script>
七、实战案例:用户管理
综合运用所学知识,用Vue 3 <script setup> 实现一个用户管理组件:
<template>
<div class="user-manager">
<h2>用户管理</h2>
<!-- 添加用户 -->
<div class="add-section">
<input
v-model="newName"
placeholder="输入用户名"
@keyup.enter="addUser"
/>
<button @click="addUser" :disabled="!canAdd">添加</button>
</div>
<!-- 搜索 -->
<div class="search-section">
<input v-model="keyword" placeholder="搜索用户..." />
</div>
<!-- 统计 -->
<div class="stats">
<span>总数:{{ users.length }}</span>
<span>活跃:{{ activeCount }}</span>
<span>结果:{{ filteredUsers.length }}</span>
</div>
<!-- 用户列表 -->
<ul class="user-list">
<li
v-for="user in filteredUsers"
:key="user.id"
:class="{ active: user.isActive }"
>
<span>{{ user.name }}</span>
<span class="status" @click="toggleStatus(user)">
{{ user.isActive ? '🟢' : '🔴' }}
</span>
<button @click="removeUser(user.id)">删除</button>
</li>
</ul>
<div v-if="users.length === 0" class="empty">暂无用户</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
// =================== 数据 ===================
const users = ref([
{ id: 1, name: '张三', isActive: true },
{ id: 2, name: '李四', isActive: false },
{ id: 3, name: '王五', isActive: true }
])
const newName = ref('')
const keyword = ref('')
let nextId = 4
// =================== 计算属性 ===================
const canAdd = computed(() => newName.value.trim().length >= 2)
const activeCount = computed(() =>
users.value.filter(u => u.isActive).length
)
const filteredUsers = computed(() => {
if (!keyword.value.trim()) return users.value
const kw = keyword.value.toLowerCase()
return users.value.filter(u =>
u.name.toLowerCase().includes(kw)
)
})
// =================== 侦听器 ===================
watch(users, (val) => {
localStorage.setItem('users', JSON.stringify(val))
}, { deep: true })
// =================== 生命周期 ===================
onMounted(() => {
const saved = localStorage.getItem('users')
if (saved) users.value = JSON.parse(saved)
})
// =================== 方法 ===================
const addUser = () => {
if (!canAdd.value) return
users.value.push({
id: nextId++,
name: newName.value.trim(),
isActive: false
})
newName.value = ''
}
const removeUser = (id) => {
const idx = users.value.findIndex(u => u.id === id)
if (idx > -1) users.value.splice(idx, 1)
}
const toggleStatus = (user) => {
user.isActive = !user.isActive
}
</script>
<style scoped>
.user-manager {
max-width: 400px;
margin: 20px auto;
padding: 20px;
font-family: system-ui, sans-serif;
}
h2 { color: #42b983; text-align: center; }
.add-section, .search-section {
display: flex;
gap: 10px;
margin: 15px 0;
}
input {
flex: 1;
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 6px;
}
input:focus {
outline: none;
border-color: #42b983;
}
button {
padding: 8px 16px;
background: #42b983;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
button:disabled { background: #ccc; cursor: not-allowed; }
.stats {
display: flex;
justify-content: space-between;
padding: 10px;
background: #f5f5f5;
border-radius: 6px;
font-size: 14px;
}
.user-list {
list-style: none;
padding: 0;
}
.user-list li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
margin: 8px 0;
background: #f9f9f9;
border-radius: 6px;
}
.user-list li.active {
background: #f0fdf4;
border-left: 3px solid #42b983;
}
.status { cursor: pointer; }
.empty {
text-align: center;
color: #999;
padding: 30px;
}
</style>
八、总结
今天我们深入学习了Vue实例与数据绑定,核心要点:
| 主题 | <script setup> 写法 | 关键点 |
|---|---|---|
| 数据 | ref() / reactive() | ref需要.value,reactive不需要 |
| 计算属性 | computed(() => {}) | 有缓存,适合数据派生 |
| 侦听器 | watch(source, callback) | 支持异步,适合副作用 |
| 生命周期 | onMounted() 等 | setup阶段直接执行代码 |
记住这些要点:
- 新项目推荐使用
<script setup>语法糖 ref是万能选择,reactive仅用于对象- 能用计算属性就不用侦听器
- 在
onUnmounted中清理副作用
下一站预告
在下一篇文章《模板语法与指令详解》中,我们将学习:
- 模板语法详解
- 常用指令(v-if、v-for、v-bind等)
- 自定义指令开发
敬请期待!
作者:洋洋技术笔记
发布日期:2026-02-28
系列:Vue.js从入门到精通 - 第2篇