Vue实例与数据绑定

0 阅读10分钟

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 APIVue 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)触发时机
代码直接执行createdcreated实例创建完成
onBeforeMountbeforeMountbeforeMountDOM挂载前
onMountedmountedmountedDOM挂载完成
onBeforeUpdatebeforeUpdatebeforeUpdate数据变化DOM更新前
onUpdatedupdatedupdatedDOM更新完成
onBeforeUnmountbeforeUnmountbeforeDestroy实例卸载前
onUnmountedunmounteddestroyed实例卸载后

2.4 使用场景速查

场景推荐钩子示例
发起API请求onMounted 或直接执行获取初始数据
操作DOMonMounted初始化图表库
设置定时器onMounted轮询、倒计时
清理定时器onUnmounted防止内存泄漏
监听窗口事件onMounted + onUnmountedresize、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>

选择建议

场景推荐原因
基本类型refreactive不支持基本类型
对象refreactive都可以,ref更统一
需要整体替换refstate.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阶段直接执行代码

记住这些要点

  1. 新项目推荐使用<script setup>语法糖
  2. ref是万能选择,reactive仅用于对象
  3. 能用计算属性就不用侦听器
  4. onUnmounted中清理副作用

下一站预告

在下一篇文章《模板语法与指令详解》中,我们将学习:

  • 模板语法详解
  • 常用指令(v-if、v-for、v-bind等)
  • 自定义指令开发

敬请期待!


作者:洋洋技术笔记
发布日期:2026-02-28
系列:Vue.js从入门到精通 - 第2篇

Vue实例与数据绑定详解 | Vue3生命周期、ref、computed与watch完整指南