2. 语音基础

好,我们正式开始接触 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 的协作关系:

MVVM 模式核心结构 Model 数据层 (录音文本、状态) ViewModel Vue 实例 (自动同步桥梁) View DOM 视图 (页面模板) 数据变化 自动更新 用户操作 更新数据 关键:ViewModel 是核心,它监听 Model 变化并驱动 View 更新 同时监听 View 的用户操作并同步回 Model 数据流方向:Model → ViewModel → View(单向数据流) 事件流方向:View → ViewModel → Model(通过事件)

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 页面了。