现在位置: 首页 > Vue3 教程 > 正文

响应式数据

响应式是 Vue3 最核心的概念。

本章你将理解什么是响应式,学会用 refreactive 管理数据,并实现分类筛选功能。


什么是响应式?

响应式的意思是:当数据变化时,页面自动更新

传统的 JS 写法是这样的:修改了变量,还得手动去更新 DOM。

而 Vue3 的响应式系统会自动追踪数据变化,并通知视图刷新——你只需要关心数据,不用操心 DOM。

实例

<script setup>
import { ref } from 'vue'

// ref 包裹的变量是响应式的
const count = ref(0)

// 修改 count 后,模板中的 {{ count }} 会自动更新
function increment() {
  count.value++  // 注意:在 JS 中需要 .value 访问
}
</script>

<template>
  <p>点击次数:{{ count }}</p>
  <!-- 模板中自动解包,不需要 .value -->
  <button @click="increment">+1</button>
</template>

点击按钮后,页面上显示的数字会自动变化——这就是响应式。

你不需要写任何 document.querySelectorinnerHTML 来更新页面。


ref — 处理基本类型

ref 是 Vue3 中最常用的响应式 API,适合包装基本类型数据。

基本类型包括:字符串、数字、布尔值、null、undefined。

语法

实例

<script setup>
import { ref } from 'vue'

// 创建 ref
const name = ref('RUNOOB')      // 字符串
const age = ref(18)             // 数字
const isVip = ref(false)        // 布尔值

// 读取 .value
console.log(name.value)         // "RUNOOB"
console.log(age.value)          // 18

// 修改 .value
name.value = 'Hello RUNOOB'
age.value++
isVip.value = !isVip.value
</script>

<template>
  <p>姓名:{{ name }}</p>
  <!-- 在模板中,ref 会自动解包,不需要 .value -->
  <p>年龄:{{ age }}</p>
  <p>VIP:{{ isVip }}</p>
</template>

最关键的记忆点:JS 代码中要加 .value,模板中不用加。这是初学者最常犯的错。


reactive — 处理对象和数组

reactive 适合包装对象或数组这类引用类型数据。

与 ref 不同,reactive 不需要 .value,直接访问属性即可。

实例

<script setup>
import { reactive } from 'vue'

// 用 reactive 包装对象
const user = reactive({
  name: '小明',
  age: 20,
  hobbies: ['编程', '跑步']
})

// 直接访问和修改,不需要 .value
console.log(user.name)           // "小明"
user.name = '小华'               // 响应式更新
user.hobbies.push('阅读')        // 数组操作也是响应式的
</script>

<template>
  <p>姓名:{{ user.name }}</p>
  <p>年龄:{{ user.age }}</p>
  <ul>
    <li v-for="(hobby, index) in user.hobbies" :key="index">
      {{ hobby }}
    </li>
  </ul>
</template>

reactive 的两个限制

限制说明解决方案
只能包装对象类型不能传入字符串、数字等基本类型基本类型用 ref
不能整体替换不能把整个对象赋值给一个新对象使用 Object.assign() 或改用 ref

实例

import { reactive, ref } from 'vue'

// 错误:不能整体替换 reactive 对象
const state = reactive({ count: 0 })
// state = { count: 1 }    // 这样不行,会失去响应式

// 正确做法1:修改属性而非替换整体
state.count = 1

// 正确做法2:用 ref 包装对象(ref 允许整体替换)
const stateRef = ref({ count: 0 })
stateRef.value = { count: 1 }  // 可以!因为 ref 的 .value 可以替换

ref vs reactive 选用时机

场景推荐示例
单个字符串/数字/布尔refconst name = ref('小明')
单个数组refconst list = ref([])
单个对象ref 或 reactive 都行const user = ref({})
多个不相干的状态各自用 ref每行一个 ref
一组强关联的状态reactive如:表单数据、配置项集合

实际开发中最常见的做法是:一律用 ref,包括对象和数组

这样写法统一,不需要在 ref 和 reactive 之间来回切换心智模型。

本教程后续章节统一使用 ref 来管理所有状态,遇到需要 .value 的地方会特别标注。


computed — 计算属性

computed 是「根据已有数据计算出来的数据」。

它有两个核心特性:缓存——依赖不变时不会重复计算;自动追踪——依赖变化时自动更新。

实例

<script setup>
import { ref, computed } from 'vue'

const articles = ref([
  { id: 1, title: 'Vue3 入门', category: 'Vue' },
  { id: 2, title: 'Promise 详解', category: 'JavaScript' },
  { id: 3, title: 'Grid 布局', category: 'CSS' },
  { id: 4, title: 'Composition API', category: 'Vue' }
])

const activeCategory = ref('全部')

// computed 根据 activeCategory 过滤文章
const filteredArticles = computed(() => {
  if (activeCategory.value === '全部') {
    return articles.value
  }
  return articles.value.filter(a => a.category === activeCategory.value)
})

// computed 也可以用来统计
const totalCount = computed(() => articles.value.length)
const filteredCount = computed(() => filteredArticles.value.length)
</script>

<template>
  <p>共 {{ totalCount }} 篇文章,当前显示 {{ filteredCount }} 篇</p>

  <!-- 分类按钮 -->
  <button @click="activeCategory = '全部'">全部</button>
  <button @click="activeCategory = 'Vue'">Vue</button>
  <button @click="activeCategory = 'JavaScript'">JavaScript</button>
  <button @click="activeCategory = 'CSS'">CSS</button>

  <!-- 过滤后的文章列表 -->
  <div v-for="article in filteredArticles" :key="article.id">
    <h3>{{ article.title }}</h3>
    <span>{{ article.category }}</span>
  </div>
</template>

每当 activeCategory 变化,filteredArticles 会自动重新计算,页面自动刷新。

注意:computed 返回的是一个 ref,所以在 JS 中访问也需要 .value。但上面的例子中我们直接在模板里用了,模板中会自动解包。


动手:给博客加上分类筛选

把 computed 应用到博客首页,实现点击分类按钮过滤文章。

实例

<script setup>
import { ref, computed } from 'vue'

const articles = ref([
  { id: 1, title: 'Vue3 入门完全指南', summary: '从零开始学 Vue3', category: 'Vue', date: '2024-05-10' },
  { id: 2, title: 'JS 异步编程详解', summary: '搞懂 Promise 和 async/await', category: 'JavaScript', date: '2024-05-08' },
  { id: 3, title: 'CSS Grid 布局实战', summary: '用 Grid 实现响应式布局', category: 'CSS', date: '2024-05-05' },
  { id: 4, title: 'Vue3 响应式原理', summary: '深入理解 ref 和 reactive', category: 'Vue', date: '2024-05-03' },
  { id: 5, title: 'Flexbox 完全指南', summary: '一文学会弹性布局', category: 'CSS', date: '2024-05-01' },
])

// 所有分类(去重)
const categories = computed(() => {
  const cats = articles.value.map(a => a.category)
  return ['全部', ...new Set(cats)]  // 前面加上「全部」
})

// 当前选中的分类
const activeCategory = ref('全部')

// 根据分类过滤文章
const filteredArticles = computed(() => {
  if (activeCategory.value === '全部') {
    return articles.value
  }
  return articles.value.filter(a => a.category === activeCategory.value)
})
</script>

<template>
  <div class="home">
    <h2>最新文章</h2>

    <!-- 分类筛选按钮 -->
    <div class="category-bar">
      <button
        v-for="cat in categories"
        :key="cat"
        :class="{ active: activeCategory === cat }"
        @click="activeCategory = cat"
      >
        {{ cat }}
      </button>
    </div>

    <!-- 筛选结果数量 -->
    <p class="result-info">共 {{ filteredArticles.length }} 篇</p>

    <!-- 文章列表 -->
    <div class="article-grid">
      <div v-for="article in filteredArticles" :key="article.id" class="card">
        <span class="tag">{{ article.category }}</span>
        <h3>{{ article.title }}</h3>
        <p>{{ article.summary }}</p>
        <span class="date">{{ article.date }}</span>
      </div>
    </div>
  </div>
</template>

<style scoped>
.category-bar {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.category-bar button {
  padding: 6px 16px;
  border: 1px solid #ddd;
  border-radius: 20px;
  background: #fff;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.2s;
}

.category-bar button.active {
  background: #42b883;
  color: #fff;
  border-color: #42b883;
}

.category-bar button:hover {
  border-color: #42b883;
}

.result-info {
  color: #999;
  font-size: 14px;
  margin-bottom: 16px;
}
</style>

现在点击不同的分类按钮,文章列表会实时过滤。你不需要写任何 DOM 操作代码,只需要关注数据变化的逻辑。


本章小结

本章你掌握了 Vue3 响应式系统的三个核心 API:ref 包装基本类型(JS 中用 .value)、reactive 包装对象、computed 派生计算数据。

通过 computed + 分类筛选按钮,你体验了「数据驱动视图」的开发模式——改变数据,页面自动更新。