2. Vue.js 核心概念(上):理解 MVVM 模式,学习数据绑定、指令、事件处理
好,我们正式开始接触 Vue.js 的核心。这一章我会带你搞懂 Vue 最根本的运作模式——MVVM,以及日常开发中你几乎每时每刻都在用的几个指令和事件处理。
说实话,我刚从 jQuery 转到 Vue 时,最不习惯的就是「数据驱动视图」这个思路。以前我要手动操作 DOM,现在只需要关心数据变了没。这个转变,就是 MVVM 带来的。
2.1 什么是 MVVM?
MVVM 是 Model-View-ViewModel 的缩写。说白了,它是一种分层架构,把界面和业务逻辑拆开。
- Model(模型):你的数据层。比如语音识别返回的文本、录音状态、音量值。
- View(视图):你看到的页面,也就是 DOM。
- ViewModel(视图模型):Vue 实例扮演的角色。它负责把 Model 和 View 自动同步起来。
你想想看,以前用 jQuery 写一个输入框联动,你得监听 input 事件,然后手动更新另一个元素的文本。在 Vue 里,你只需要声明数据,然后告诉模板「用这个数据」,剩下的 Vue 帮你搞定。
核心理解:MVVM 的本质是「声明式渲染」。你只管描述「视图应该长什么样」,不用管「怎么去更新它」。ViewModel 就是那个自动同步的桥梁。
我在项目中遇到过不少新人,上来就写 document.getElementById 去改数据。嗯,这其实是在跟 MVVM 对着干。Vue 的规矩是:改数据,别改 DOM。
下面这张图能帮你直观理解 MVVM 的协作关系:
2.2 数据绑定:把数据「粘」到视图上
Vue 的数据绑定分两种:单向绑定和双向绑定。理解它们的区别,是你用好 Vue 的第一步。
2.2.1 单向绑定:v-bind
v-bind 用于把数据绑定到 HTML 属性上。比如动态绑定一个 src、href、class 或 style。
<!-- 基本用法 -->
<img v-bind:src="imageUrl" alt="语音波形" />
<!-- 缩写语法(我几乎只用缩写) -->
<a :href="#" aria-disabled="true" data-keke-missing-local-link="1" onclick="return false">下载录音文件</a>
<!-- 动态绑定 class -->
<div :class="{ active: isRecording, disabled: !isRecording }">
录音状态指示器
</div>
我个人习惯用缩写 :,代码更干净。但团队里如果有新人,我会建议先写完整版,等熟悉了再换缩写。
小技巧:v-bind 绑定的值可以是表达式。比如 :class="['btn', type === 'primary' ? 'btn-primary' : 'btn-default']"。这在做语音识别状态切换时特别有用。
2.2.2 双向绑定:v-model
v-model 是 Vue 的「语法糖」。它把 v-bind 和 v-on:input 合并成了一个指令。主要用于表单元素。
<!-- 输入框双向绑定 -->
<input v-model="searchText" placeholder="搜索语音记录" />
<!-- 等价于 -->
<input :value="searchText" @input="searchText = $event.target.value" />
你想想看,如果每次都要手动写 @input 事件去更新数据,那得多累。v-model 帮你省掉了这一步。
注意:v-model 只能用于表单元素(input、textarea、select)和自定义组件。你不能在 <div> 上用 v-model。我曾经见过有人试图在 <div contenteditable> 上用 v-model,结果踩坑了——它不生效,得自己封装。
2.3 条件渲染:v-if 与 v-show
做语音识别系统时,很多界面元素需要根据状态显示或隐藏。比如「录音中」显示波形图,「未录音」显示开始按钮。Vue 提供了两个指令:v-if 和 v-show。
| 特性 | v-if | v-show |
|---|---|---|
| 渲染方式 | 条件为 false 时,DOM 元素被销毁 | 条件为 false 时,元素保留,仅切换 display: none |
| 初始开销 | 惰性渲染,条件为 true 时才创建 | 初始时全部渲染 |
| 切换开销 | 每次切换都销毁/重建 DOM | 只切换 CSS,开销极小 |
| 适用场景 | 条件很少改变(如登录/未登录) | 条件频繁切换(如录音状态指示器) |
<!-- v-if:条件很少变化时用 -->
<div v-if="isLoggedIn">
欢迎回来,{{ username }}
</div>
<div v-else>
请先登录
</div>
<!-- v-show:频繁切换时用 -->
<div v-show="isRecording" class="recording-indicator">
🔴 录音中...
</div>
我在项目中遇到过一个问题:录音状态指示器用 v-if 实现,结果每次开始录音时页面都会闪一下。换成 v-show 后,问题立刻消失。原因就是 v-if 每次都在重建 DOM,而 v-show 只是隐藏/显示。
经验之谈:如果元素在页面生命周期内会频繁切换(每秒几次甚至更多),用 v-show。如果切换频率很低(比如用户登录状态切换),用 v-if 更省内存。
2.4 列表渲染:v-for
语音识别系统里,你经常需要渲染列表:历史记录、识别结果候选词、音频文件列表。v-for 就是干这个的。
<!-- 遍历数组 -->
<ul>
<li v-for="(item, index) in historyList" :key="item.id">
{{ index + 1 }}. {{ item.text }}({{ item.time }})
</li>
</ul>
<!-- 遍历对象 -->
<div v-for="(value, key) in recognitionResult" :key="key">
{{ key }}: {{ value }}
</div>
必须记住:用 v-for 时,一定要绑定 :key。key 应该是唯一标识,比如数据的 id。如果不加 key,Vue 会复用 DOM 元素,导致列表更新时出现诡异的问题。我曾经因为忘了加 key,排查了整整两个小时……
2.5 事件处理:v-on
用户点击按钮、输入文字、拖拽文件,这些交互都需要事件处理。Vue 用 v-on 指令来监听 DOM 事件。
<!-- 基本用法 -->
<button v-on:click="startRecording">开始录音</button>
<!-- 缩写(我推荐用这个) -->
<button @click="stopRecording">停止录音</button>
<!-- 传递参数 -->
<button @click="deleteRecord(record.id)">删除</button>
<!-- 访问原生事件对象 -->
<input @keyup.enter="search" placeholder="按回车搜索" />
Vue 还提供了一些事件修饰符,能帮你省掉不少代码:
.stop:阻止事件冒泡(等价于event.stopPropagation()).prevent:阻止默认行为(等价于event.preventDefault()).once:事件只触发一次.enter、.esc等:键盘按键修饰符
<!-- 阻止表单提交的默认行为 -->
<form @submit.prevent="submitForm">
<input v-model="query" />
<button type="submit">搜索</button>
</form>
<!-- 只触发一次 -->
<button @click.once="sendFeedback">提交反馈</button>
我的习惯:事件处理函数名用动词开头,比如 handleClick、onSubmit、startRecording。这样一看就知道是做什么的。别用 click1、func2 这种名字,维护起来很痛苦。
2.6 综合示例:一个简单的录音控制面板
把上面学的知识点串起来,写一个迷你录音控制面板:
<template>
<div class="recorder-panel">
<h3>语音识别控制台</h3>
<!-- 状态显示 -->
<p>状态:{{ isRecording ? '录音中' : '待机' }}</p>
<!-- 录音按钮 -->
<button
@click="toggleRecording"
:class="{ 'btn-danger': isRecording, 'btn-primary': !isRecording }">
{{ isRecording ? '停止录音' : '开始录音' }}
</button>
<!-- 录音时长(仅录音时显示) -->
<p v-show="isRecording">
已录制:{{ duration }} 秒
</p>
<!-- 历史记录列表 -->
<ul v-if="records.length > 0">
<li v-for="record in records" :key="record.id">
{{ record.text }}
<button @click="deleteRecord(record.id)">删除</button>
</li>
</ul>
<p v-else>暂无录音记录</p>
</div>
</template>
<script>
export default {
data() {
return {
isRecording: false,
duration: 0,
records: []
}
},
methods: {
toggleRecording() {
this.isRecording = !this.isRecording
if (this.isRecording) {
this.startTimer()
} else {
this.stopTimer()
}
},
deleteRecord(id) {
this.records = this.records.filter(r => r.id !== id)
}
}
}
</script>
这个例子用到了:v-bind 动态绑定 class、v-show 控制显示、v-if/v-else 条件渲染、v-for 列表渲染、@click 事件处理。你看,核心概念就这么几个,组合起来就能做出一个完整的功能模块。
总结一下:MVVM 让你从「操作 DOM」变成「操作数据」。v-bind 把数据单向绑到视图,v-model 实现双向绑定,v-if/v-show 控制显示,v-for 渲染列表,v-on 处理交互。掌握这五个指令,你已经能写大部分 Vue 页面了。