响应式数据
响应式是 Vue3 最核心的概念。
本章你将理解什么是响应式,学会用 ref 和 reactive 管理数据,并实现分类筛选功能。
什么是响应式?
响应式的意思是:当数据变化时,页面自动更新。
传统的 JS 写法是这样的:修改了变量,还得手动去更新 DOM。
而 Vue3 的响应式系统会自动追踪数据变化,并通知视图刷新——你只需要关心数据,不用操心 DOM。
实例
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.querySelector 或 innerHTML 来更新页面。
ref — 处理基本类型
ref 是 Vue3 中最常用的响应式 API,适合包装基本类型数据。
基本类型包括:字符串、数字、布尔值、null、undefined。
语法
实例
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,直接访问属性即可。
实例
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 |
实例
// 错误:不能整体替换 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 选用时机
| 场景 | 推荐 | 示例 |
|---|---|---|
| 单个字符串/数字/布尔 | ref | const name = ref('小明') |
| 单个数组 | ref | const list = ref([]) |
| 单个对象 | ref 或 reactive 都行 | const user = ref({}) |
| 多个不相干的状态 | 各自用 ref | 每行一个 ref |
| 一组强关联的状态 | reactive | 如:表单数据、配置项集合 |
实际开发中最常见的做法是:一律用 ref,包括对象和数组。
这样写法统一,不需要在 ref 和 reactive 之间来回切换心智模型。
本教程后续章节统一使用 ref 来管理所有状态,遇到需要
.value的地方会特别标注。
computed — 计算属性
computed 是「根据已有数据计算出来的数据」。
它有两个核心特性:缓存——依赖不变时不会重复计算;自动追踪——依赖变化时自动更新。
实例
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 应用到博客首页,实现点击分类按钮过滤文章。
实例
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 + 分类筛选按钮,你体验了「数据驱动视图」的开发模式——改变数据,页面自动更新。
