3、模板语法与指令:插值表达式、v-bind、v-model、v-for、v-if/v-show、事件处理

模板语法,说白了就是 Vue 用来把数据「映射」到 DOM 上的那套规则。我刚接触 Vue 时,觉得这东西不就是写写花括号嘛?后来才发现,这里面的门道还真不少。今天咱们就把最核心的六个知识点掰开揉碎聊一聊。

3.1 插值表达式:最基础的「数据填坑」

插值表达式用双花括号 {{ }} 包裹。它的作用很简单——把 JavaScript 表达式的计算结果渲染到页面上。

<template>
  <p>{{ message }}</p>
  <p>{{ count + 1 }}</p>
  <p>{{ isActive ? '激活' : '未激活' }}</p>
</template>

<script setup>
const message = '你好,Vue3!'
const count = 10
const isActive = true
</script>
小提示:插值表达式里只能写「单行表达式」,不能写 if/for 等语句。我见过有人往里塞 if (x) { ... },结果页面直接报错。记住,它只负责取值,不负责逻辑控制。

3.2 v-bind:动态绑定 HTML 属性

有时候你需要动态控制一个标签的属性,比如图片的 src、按钮的 disabled。这时候 v-bind 就派上用场了。它可以把数据绑定到 HTML 属性上。

<template>
  &img :src="imageUrl" alt="动态图片" />
  <button :disabled="isSubmitting">提交</button>
  <div :class="{ active: isActive, error: hasError }">状态盒子</div>
</template>

<script setup>
const imageUrl = 'https://example.com/logo.png'
const isSubmitting = true
const isActive = false
const hasError = true
</script>

v-bind: 可以简写为 :。我个人习惯一直用简写,少打几个字,代码也更清爽。class 和 style 的绑定比较特殊,支持对象和数组语法,这在做动态样式时非常实用。

注意:布尔属性(如 disabled、checked)绑定时,如果值为 nullundefined,该属性会被移除。我曾经踩过这个坑——以为传个 false 就行,结果属性还在,只是值变成了 false 字符串。

3.3 v-model:双向绑定的「魔法」

v-model 是 Vue 最让人舒服的特性之一。它把数据绑定和事件监听合二为一。说白了,就是「数据变,视图变;视图变,数据也变」。

<template>
  <input v-model="username" placeholder="请输入用户名" />
  <p>你输入的是:{{ username }}</p>

  <!-- 等价于下面这种写法 -->
  <input :value="username" @input="username = $event.target.value" />
</template>

<script setup>
import { ref } from 'vue'
const username = ref('')
</script>

在 Vue3 中,v-model 默认绑定的是 modelValue 属性,触发的是 update:modelValue 事件。如果你自定义组件,记得按这个约定来。另外,Vue3 允许一个组件上使用多个 v-model,比如 v-model:titlev-model:content,这在封装复杂表单组件时特别好用。

核心要点:v-model 的本质是语法糖。它帮你省去了手动绑定 value 和监听 input 事件的步骤。但如果你需要做额外的处理(比如防抖、格式化),还是得拆开来写。

3.4 v-for:列表渲染的「循环器」

渲染列表数据,v-for 是标配。它支持遍历数组和对象。

<template>
  <ul>
    <li v-for="(item, index) in items" :key="item.id">
      {{ index + 1 }}. {{ item.name }}
    </li>
  </ul>

  <!-- 遍历对象 -->
  <div v-for="(value, key, index) in user" :key="key">
    {{ key }}: {{ value }}
  </div>
</template>

<script setup>
const items = [
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' },
  { id: 3, name: '橘子' }
]
const user = { name: '张三', age: 28, city: '北京' }
</script>

这里有个关键点——:key 必须加。为什么?Vue 的虚拟 DOM 靠 key 来识别节点,没有 key 或者用 index 当 key,在列表增删时会出现渲染错乱。我在项目中遇到过:用 index 做 key,结果删除第二项后,第三项的 input 框内容跑到了第二项上。嗯,从那以后我再也不敢偷懒了。

建议:key 最好用唯一标识(如 id、uuid),不要用 index。如果数据来自后端,确保每条数据都有唯一 id。

3.5 v-if / v-show:条件渲染的「双胞胎」

这两个指令都用来控制元素的显示与隐藏,但机制完全不同。

  • v-if:真正的条件渲染。条件为假时,元素根本不会出现在 DOM 中。
  • v-show:只是切换 CSS 的 display 属性。元素始终在 DOM 中。
<template>
  <div v-if="isLoggedIn">欢迎回来,用户!</div>
  <div v-else>请先登录</div>

  <div v-show="isVisible">我始终在 DOM 中,只是被隐藏了</div>
</template>

<script setup>
const isLoggedIn = false
const isVisible = true
</script>

怎么选?我的经验是:如果元素频繁切换(比如选项卡、弹窗),用 v-show 性能更好。如果切换不频繁,或者条件判断涉及复杂的计算,用 v-if 更合适。另外,v-if 可以和 v-else-ifv-else 配合使用,形成多分支条件。

注意:v-if 和 v-for 不要用在同一个元素上。因为 v-for 的优先级比 v-if 高,会导致每次渲染都先循环再判断,性能很差。正确的做法是:在外层包一个 <template> 标签,把 v-if 放在上面。

3.6 事件处理:让页面「活」起来

没有事件,页面就是一张静态海报。Vue 用 v-on(简写 @)来绑定事件。

<template>
  <button @click="handleClick">点击我</button>
  <input @keyup.enter="handleEnter" placeholder="按回车触发" />
  <div @click.stop="handleInner">阻止冒泡</div>
</template>

<script setup>
const handleClick = (event) => {
  console.log('按钮被点击了', event)
}

const handleEnter = (e) => {
  console.log('你按了回车,输入内容是:', e.target.value)
}

const handleInner = () => {
  console.log('只触发内部点击,不会冒泡到父元素')
}
</script>

Vue 提供了丰富的事件修饰符:.stop(阻止冒泡)、.prevent(阻止默认行为)、.once(只触发一次)、.capture(捕获模式)等。还有按键修饰符,比如 .enter.esc.delete。这些修饰符可以链式使用,比如 @click.stop.prevent

个人经验:事件处理函数尽量保持简洁,只做「事件分发」的工作。复杂的业务逻辑抽到单独的函数里。这样代码好维护,也方便写单元测试。

3.7 综合示例:一个简单的待办事项列表

把上面这些知识点串起来,写一个简单的待办事项列表。你想想看,是不是正好用到了插值、v-bind、v-model、v-for、v-if 和事件处理?

<template>
  <div>
    <h2>我的待办事项</h2>
    <input v-model="newTodo" @keyup.enter="addTodo" placeholder="输入新事项,按回车添加" />
    <ul>
      <li v-for="(todo, index) in todos" :key="todo.id">
        <span :class="{ completed: todo.done }" @click="toggleTodo(index)">
          {{ todo.text }}
        </span>
        <button @click="removeTodo(index)">删除</button>
      </li>
    </ul>
    <p v-if="todos.length === 0">暂无待办事项,添加一个吧!</p>
    <p v-show="todos.length > 0">共 {{ todos.length }} 项</p>
  </div>
</template>

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

const newTodo = ref('')
const todos = ref([
  { id: 1, text: '学习 Vue3 模板语法', done: false },
  { id: 2, text: '练习指令用法', done: true }
])

let nextId = 3

const addTodo = () => {
  if (newTodo.value.trim() === '') return
  todos.value.push({
    id: nextId++,
    text: newTodo.value,
    done: false
  })
  newTodo.value = ''
}

const toggleTodo = (index) => {
  todos.value[index].done = !todos.value[index].done
}

const removeTodo = (index) => {
  todos.value.splice(index, 1)
}
</script>

这个例子虽然简单,但涵盖了模板语法的核心用法。你可以在本地跑一下,感受一下数据驱动视图的魅力。说白了,Vue 就是帮你把「数据」和「界面」之间的映射关系管理好,剩下的就是写业务逻辑了。

最后说一句:模板语法是 Vue 的基石。别觉得简单就跳过,多写几个小例子,把每个指令的边界情况摸清楚。我在项目里见过太多因为 v-if 和 v-show 用错导致的性能问题,或者 v-for 没加 key 导致的诡异 bug。基础打牢了,后面学组件通信、路由、状态管理才会顺手。