Vue实例与数据绑定

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中,创建应用实例的方式:

javascript 复制代码
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

1.2 组件的"五脏六腑"

一个完整的Vue组件可以包含以下部分。先看Vue 3 <script setup>语法糖写法(推荐):

vue 复制代码
<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写法

vue 复制代码
<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写法(已过时,了解即可):

vue 复制代码
<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,直接使用响应式变量即可:

vue 复制代码
<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访问:

javascript 复制代码
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++      // 需要this
      this.log()        // 需要this
    },
    log() {
      console.log(this.count)
    }
  }
}

Options API的常见陷阱 :箭头函数没有自己的this

javascript 复制代码
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    // ❌ 错误:箭头函数的this不指向Vue实例
    wrongIncrement: () => {
      this.count++      // 报错!
    },
    // ✅ 正确:普通函数
    correctIncrement() {
      this.count++
    }
  }
}

💡 <script setup>的优势 :彻底告别this的烦恼,代码更简洁,类型推断更友好。


二、生命周期:Vue实例的"人生旅程"

每个Vue实例都有完整的生命周期------从创建到销毁,就像人的一生。理解生命周期,你就能在正确的时机做正确的事。

2.1 生命周期全景图

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                      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> 写法(推荐):

vue 复制代码
<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写法

vue 复制代码
<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写法

vue 复制代码
<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> 写法

vue 复制代码
<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不支持基本类型
对象 refreactive 都可以,ref更统一
需要整体替换 ref state.value = newObj
解构需求 reactive + toRefs 保持响应性

3.3 响应式陷阱与解决

陷阱一:解构丢失响应性

vue 复制代码
<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被整体替换

vue 复制代码
<script setup>
import { reactive } from 'vue'

const state = reactive({ count: 0 })

// ❌ 错误:整体替换会丢失响应性
const wrongReset = () => {
  state = { count: 0 }    // state不再是响应式的
}

// ✅ 正确:修改属性
const rightReset = () => {
  state.count = 0
}
</script>

陷阱三:ref在模板中的自动解包

vue 复制代码
<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> 写法

vue 复制代码
<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写法

javascript 复制代码
export default {
  data() {
    return {
      price: 100,
      quantity: 2,
      discount: 0.8
    }
  },
  computed: {
    totalPrice() {
      return this.price * this.quantity * this.discount
    }
  }
}

4.2 计算属性 vs 方法

vue 复制代码
<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:

vue 复制代码
<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> 写法

vue 复制代码
<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写法

javascript 复制代码
export default {
  data() {
    return {
      searchKeyword: '',
      searchResults: []
    }
  },
  watch: {
    searchKeyword(newVal, oldVal) {
      console.log(`从 "${oldVal}" 变为 "${newVal}"`)
      this.searchResults = []
    }
  }
}

5.2 监听选项

vue 复制代码
<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 监听对象属性

vue 复制代码
<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 实战:搜索防抖

vue 复制代码
<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,自动追踪回调中使用的响应式数据:

vue 复制代码
<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 选择指南

用计算属性

  • 根据已有数据计算新数据
  • 需要缓存避免重复计算
  • 纯函数,无副作用
vue 复制代码
<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>

用侦听器

  • 需要执行异步操作
  • 数据变化时执行副作用
  • 需要比较新旧值
vue 复制代码
<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> 实现一个用户管理组件:

vue 复制代码
<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完整指南

相关推荐
Marshall1511 小时前
zzy-scroll-timer:一个跨框架的滚动定时器插件
前端·javascript
明月_清风3 小时前
打字机效果优化:用 requestAnimationFrame 缓冲高频文字更新
前端·javascript
明月_清风3 小时前
Markdown 预解析:别等全文完了再渲染,如何流式增量渲染代码块和公式?
前端·javascript
掘金安东尼4 小时前
用 CSS 打造完美的饼图
前端·css
掘金安东尼11 小时前
纯 CSS 实现弹性文字效果
前端·css
牛奶12 小时前
Vue 基础理论 & API 使用
前端·vue.js·面试
牛奶12 小时前
Vue 底层原理 & 新特性
前端·vue.js·面试
anOnion12 小时前
构建无障碍组件之Radio group pattern
前端·html·交互设计
pe7er12 小时前
状态提升:前端开发中的状态管理的设计思想
前端·vue.js·react.js