在 1.2 节我们提过,Vue 的核心设计思想之一是“数据驱动视图”。这一节,我们不谈原理,直接通过最简短的代码来感受这种工作方式——你会发现,原来让页面动起来可以这么简单。
一个会自己更新的计数器
假设你正在写一个页面,上面显示一个数字,下面有个按钮,点一下数字就加 1。用原生 JavaScript 写,大概是这样的:
<span id="num">0</span>
<button id="btn">+1</button>
<script>
let count = 0;
const span = document.getElementById('num');
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
count++;
span.textContent = count; // 手动更新 DOM
});
</script>
你改完数据(count++)之后,必须再手动找到对应的 DOM 节点,把新值塞进去。数据是数据,视图是视图,中间需要你来当“接线员”。
现在换成 Vue,在单文件组件里写:
<template>
<div>
<span>{{ count }}</span>
<button @click="count++">+1</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
我们干了什么?
- 用
ref(0)声明了一个响应式数据count,初始值为0。 - 在模板里用
{{ count }}输出它的值。 - 给按钮绑定了一个点击事件
@click="count++",直接修改count。
运行结果: 点击按钮,页面上的数字自动从 0 变成 1、2、3……整个过程,我们没有写任何 document.getElementById、没有操作 textContent,甚至没有“刷新页面”的动作。改了数据,界面就跟着变,一步到位。
这就是数据驱动视图最直观的体验:数据是大脑,视图是身体,它们天然同步。你只需要关心数据怎么变,视图更新交给 Vue。
另一个例子:让输入框“活”起来
再看一个更常见的场景:一个输入框,你希望输入什么,下面就实时显示什么。
原生 JS 需要监听 input 事件,然后手动改下面段落的内容。而 Vue 里,我们用一个叫 v-model 的指令就能“绑死”数据和输入框:
<template>
<div>
<input v-model="message" placeholder="随便打字" />
<p>你输入的是:{{ message }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('')
</script>
打开页面,在输入框里敲几个字,下面段落里的文字会同步变化,没有任何延迟感。背后的逻辑是:v-model 自动监听了输入框的变化,把新值写回 message;而 message 一变,所有用到它的模板位置({{ message }})自动更新。
这种“写了一条数据,全页面跟进”的能力,就是响应式系统的价值。它让界面开发从“命令式操控 DOM”变成了“声明式描述数据关系”,你不再需要:
- 记着更新每一个关联位置
- 担心不同地方的相同数据出现不一致
- 写一堆事件监听和选择器
响应式数据的两种基本形态:ref 和 reactive
上面我们用到了 ref,它是 Vue 3 里定义基本类型(如数字、字符串、布尔值)响应式数据最常用的 API。它的语法是:
import { ref } from 'vue'
const count = ref(0) // 初始值 0
const name = ref('Alice') // 初始值 'Alice'
在 <script setup> 里,你可以直接通过 count.value 来读取或修改它的值。但在 <template> 里,Vue 会自动帮你“解包”,所以直接写 {{ count }} 就行,不用加 .value。
当你要处理一个对象或数组(引用类型)时,通常用 reactive:
import { reactive } from 'vue'
const user = reactive({
name: 'Bob',
age: 25
})
现在,user.name 和 user.age 都是响应式的。你在模板里可以直接写 {{ user.name }};在脚本里修改 user.age = 26,界面同样自动更新。
选择小技巧:
- 如果只是存一个单一值(一个数字、一个字符串),用
ref。 - 如果有一组关联的数据(比如表单字段、用户信息),放在
reactive对象里更自然。
其实你可以完全只用 ref,因为 ref 也能包住对象。但 reactive 处理对象时“不需要 .value”,用起来更贴近直觉。随着经验上来,你会有自己的偏好。
真实开发中的体验
假设你要做一个简单的任务清单:
<template>
<div>
<input v-model="newTask" @keyup.enter="addTask" placeholder="添加任务" />
<ul>
<li v-for="task in tasks" :key="task.id">{{ task.text }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const newTask = ref('')
const tasks = reactive([])
function addTask() {
if (newTask.value.trim()) {
tasks.push({
id: Date.now(),
text: newTask.value
})
newTask.value = '' // 清空输入框
}
}
</script>
这个简单的 Todo 涵盖了几个关键点:
newTask和输入框双向绑定,输入框内容变了,newTask自动跟上。- 点击回车,往
tasks数组里追加一条新任务——用的是push,这和我们操作普通数组一模一样。 - 界面通过
v-for遍历tasks,当数组新增元素时,列表自动多出一行。
整个过程你只需要关心“数组里有多少任务”“如何添加、删除任务”,视图自动根据数组内容重新渲染。你就是真正在写“业务逻辑”,而不是在写“DOM 操作”。
为什么这叫“直观”?
很多新手第一次运行上面的代码都会被这个流畅感所打动。不是因为技术有多高深,而是因为思维负担极低:你想让页面显示什么,就声明什么;想让它变,改数据就行。框架替你干了所有同步的脏活累活。
这种开发体验一旦习惯了,就再也回不去手动操作 DOM 的时代了。后面几章我们会详细拆解这套响应式系统到底是怎么运作的,但现在,你只需要知道:在 Vue 里,写界面 = 写数据 + 写模板,而你最该关注意的是数据。