Skip to content
第 114 / 250 章前端⏱ 10 分钟阅读

第 114 章:模板语法深入

学习目标

  • 深入掌握 Vue 3 模板的各种指令
  • 理解 v-if vs v-show 的差异
  • 掌握 v-model 在表单中的各种用法
  • 学会自定义指令与渲染函数

一、模板插值

1.1 文本插值

vue
<script setup>
const msg = 'hello';
</script>

<template>
  <!-- 基本文本 -->
  <p>{{ msg }}</p>

  <!-- 单个 JS 表达式 -->
  <p>{{ msg.toUpperCase() }}</p>
  <p>{{ msg.split('').reverse().join('') }}</p>
  <p>{{ Math.random() }}</p>

  <!-- 三元表达式 -->
  <p>{{ msg === 'hello' ? 'Hi' : 'Bye' }}</p>

  <!-- 调用方法 -->
  <p>{{ formatDate(new Date()) }}</p>
</template>

1.2 原始 HTML(v-html)

vue
<script setup>
const rawHtml = '<span style="color: red">红色</span>';
</script>

<template>
  <p v-html="rawHtml"></p>
</template>

警告

永远不要对用户提供的内容使用 v-html,会导致 XSS 攻击。

二、属性绑定 v-bind

2.1 基础用法

vue
<script setup>
const url = 'https://vuejs.org';
const imgSrc = '/logo.png';
const isDisabled = false;
</script>

<template>
  <!-- 完整 -->
  <a v-bind:href="url">Link</a>

  <!-- 简写 -->
  <a :href="url">Link</a>

  <!-- 动态参数名 -->
  <a :[attrName]="url">Link</a>

  <!-- 同一元素多个绑定 -->
  <img :src="imgSrc" :alt="imgAlt" />
</template>

2.2 绑定对象

vue
<script setup>
import { reactive } from 'vue';

const attrs = reactive({
  id: 'container',
  class: 'wrapper',
  'data-test': 'demo'
});
</script>

<template>
  <!-- 批量绑定属性 -->
  <div v-bind="attrs">内容</div>
  <!-- 等价于 -->
  <div :id="attrs.id" :class="attrs.class">内容</div>
</template>

2.3 class 与 style 绑定

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

const isActive = ref(true);
const hasError = ref(false);
const classObject = ref({ active: true, 'text-danger': false });

const color = ref('red');
const fontSize = ref(16);
</script>

<template>
  <!-- 对象语法 -->
  <div :class="{ active: isActive, 'text-danger': hasError }"></div>

  <!-- 数组语法 -->
  <div :class="[isActive ? 'active' : '', 'base-class']"></div>

  <!-- style 对象语法 -->
  <div :style="{ color: color, fontSize: fontSize + 'px' }"></div>

  <!-- style 数组语法 -->
  <div :style="[baseStyles, overridingStyles]"></div>
</template>

三、条件渲染

3.1 v-if 系列

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

const type = ref('A');
const user = ref<{ name: string } | null>(null);
</script>

<template>
  <!-- 基础用法 -->
  <div v-if="type === 'A'">A</div>
  <div v-else-if="type === 'B'">B</div>
  <div v-else>C</div>

  <!-- template 上使用(不会渲染 DOM) -->
  <template v-if="user">
    <h1>{{ user.name }}</h1>
    <p>欢迎回来!</p>
  </template>
</template>

3.2 v-show

vue
<template>
  <!-- 始终渲染,只是切换 display -->
  <p v-show="isVisible">Hello</p>
</template>

3.3 v-if vs v-show

维度v-ifv-show
渲染惰性:条件为真才渲染始终渲染
切换销毁/重建 DOM切换 display
性能切换开销大初始开销大
适用不常切换频繁切换
支持 template

四、列表渲染 v-for

4.1 基础用法

vue
<script setup>
const items = [
  { id: 1, name: 'Tom' },
  { id: 2, name: 'Jerry' }
];

const object = { a: 1, b: 2 };

const numbers = [1, 2, 3, 4, 5];
</script>

<template>
  <!-- 数组 -->
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>

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

  <!-- 对象 -->
  <ul>
    <li v-for="(value, key, index) in object" :key="key">
      {{ index }}. {{ key }}: {{ value }}
    </li>
  </ul>

  <!-- 数字范围 -->
  <span v-for="n in 10" :key="n">{{ n }} </span>
</template>

4.2 key 的作用

vue
<!-- ❌ 不推荐:用 index 当 key -->
<li v-for="(item, index) in items" :key="index">{{ item }}</li>

<!-- ✅ 推荐:用稳定的唯一 id -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>

key 的作用

key 帮助 Vue 识别每个节点,高效复用和重排 DOM。

4.3 v-for 与 v-if 的优先级

vue
<!-- ⚠️ 不推荐:v-if 在 v-for 之前判断 -->
<li v-for="item in items" v-if="item.visible" :key="item.id">

<!-- ✅ 推荐:用 computed 过滤 -->
<script setup>
import { computed } from 'vue';
const items = [...];
const visibleItems = computed(() => items.filter(item => item.visible));
</script>

<template>
  <li v-for="item in visibleItems" :key="item.id">
</template>

五、事件处理

5.1 基础事件

vue
<script setup>
const count = ref(0);

const increment = (event: MouseEvent) => {
  console.log(event.target);
  count.value++;
};

const greet = (name: string, event?: MouseEvent) => {
  console.log(`Hello, ${name}`, event);
};
</script>

<template>
  <!-- 内联 -->
  <button @click="count++">+</button>

  <!-- 方法处理器 -->
  <button @click="increment">+</button>

  <!-- 传参 -->
  <button @click="greet('Tom')">Greet</button>

  <!-- 访问 event -->
  <button @click="greet('Tom', $event)">Greet</button>
</template>

5.2 事件修饰符

vue
<template>
  <!-- .stop 阻止冒泡 -->
  <button @click.stop="handleClick">Stop</button>

  <!-- .prevent 阻止默认行为 -->
  <form @submit.prevent="onSubmit">Submit</form>

  <!-- .capture 捕获阶段触发 -->
  <div @click.capture="handleCapture">Capture</div>

  <!-- .self 仅当 event.target 是元素本身触发 -->
  <div @click.self="handleSelf">Self</div>

  <!-- .once 只触发一次 -->
  <button @click.once="handleOnce">Once</button>

  <!-- 键盘修饰符 -->
  <input @keyup.enter="submit" />
  <input @keyup.esc="cancel" />
  <input @keyup.tab="next" />

  <!-- 鼠标修饰符 -->
  <button @click.left="handleLeft">Left</button>
  <button @click.right.prevent="handleRight">Right</button>
</template>

5.3 系统修饰键

vue
<template>
  <!-- Ctrl + 点击 -->
  <div @click.ctrl="handleCtrl">Ctrl</div>

  <!-- Alt + Enter -->
  <input @keyup.alt.enter="submit" />

  <!-- Shift + 点击 -->
  <div @click.shift="handleShift">Shift</div>

  <!-- 精确修饰:只有指定键 -->
  <button @click.ctrl.exact="handleOnlyCtrl">Only Ctrl</button>
</template>

六、表单输入绑定 v-model

6.1 基础用法

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

const text = ref('');
const textarea = ref('');
const checked = ref(false);
const picked = ref('A');
const selected = ref<string[]>([]);
const number = ref(0);
</script>

<template>
  <!-- 文本 -->
  <input v-model="text" />
  <textarea v-model="textarea"></textarea>

  <!-- 复选框 -->
  <input type="checkbox" v-model="checked" />

  <!-- 单选 -->
  <input type="radio" v-model="picked" value="A" />
  <input type="radio" v-model="picked" value="B" />

  <!-- 选择框 -->
  <select v-model="selected">
    <option value="a">A</option>
    <option value="b">B</option>
  </select>

  <!-- 多选 -->
  <select v-model="selected" multiple>
    <option value="1">一</option>
    <option value="2">二</option>
  </select>
</template>

6.2 修饰符

vue
<template>
  <!-- .lazy 在 change 事件后同步(而不是 input) -->
  <input v-model.lazy="text" />

  <!-- .number 自动转数字 -->
  <input v-model.number="age" type="number" />

  <!-- .trim 去除首尾空格 -->
  <input v-model.trim="text" />
</template>

6.3 自定义组件 v-model

vue
<!-- CustomInput.vue -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();

const update = (e: Event) => {
  emit('update:modelValue', (e.target as HTMLInputElement).value);
};
</script>

<template>
  <input :value="modelValue" @input="update" />
</template>

<!-- 父组件使用 -->
<CustomInput v-model="search" />

6.4 多个 v-model

vue
<!-- UserForm.vue -->
<script setup lang="ts">
defineProps<{ name: string; age: number }>();
defineEmits<{
  'update:name': [value: string];
  'update:age': [value: number];
}>();
</script>

<template>
  <input :value="name" @input="$emit('update:name', $event.target.value)" />
  <input :value="age" type="number" @input="$emit('update:age', +$event.target.value)" />
</template>

<!-- 父组件 -->
<UserForm v-model:name="userName" v-model:age="userAge" />

七、自定义指令

7.1 注册全局指令

typescript
// main.ts
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 注册 v-focus 指令
app.directive('focus', {
  mounted(el) {
    el.focus();
  }
});

app.mount('#app');

7.2 注册局部指令

vue
<script setup lang="ts">
// v-focus 在模板中自动可用
const vFocus = {
  mounted(el: HTMLElement) {
    el.focus();
  }
};
</script>

<template>
  <input v-focus />
</template>

7.3 钩子函数

typescript
const vDemo = {
  // 创建时
  created(el, binding, vnode) {},

  // 挂载前
  beforeMount(el, binding, vnode) {},

  // 挂载后
  mounted(el, binding, vnode) {
    // el: 绑定的元素
    // binding.value: 传入的值
    // binding.arg: 指令参数(如 v-on:click 中的 click)
    // binding.modifiers: 修饰符对象
  },

  // 更新前
  beforeUpdate(el, binding, vnode, prevVnode) {},

  // 更新后
  updated(el, binding, vnode) {},

  // 卸载前
  beforeUnmount(el, binding, vnode) {},

  // 卸载后
  unmounted(el, binding, vnode) {}
};

7.4 实战:防抖点击

typescript
// v-debounce-click.ts
import type { Directive } from 'vue';

interface ElType extends HTMLElement {
  _debounceTimer?: number;
}

export const debounceClick: Directive<ElType, () => void> = {
  mounted(el, binding) {
    el.addEventListener('click', () => {
      if (el._debounceTimer) clearTimeout(el._debounceTimer);
      el._debounceTimer = window.setTimeout(() => {
        binding.value();
      }, 500);
    });
  },
  unmounted(el) {
    if (el._debounceTimer) clearTimeout(el._debounceTimer);
  }
};
vue
<template>
  <button v-debounce-click="handleSubmit">提交</button>
</template>

八、模板引用 ref

8.1 基础用法

vue
<script setup lang="ts">
import { ref, onMounted } from 'vue';

const inputRef = ref<HTMLInputElement | null>(null);

onMounted(() => {
  inputRef.value?.focus();
});
</script>

<template>
  <input ref="inputRef" />
</template>

8.2 v-for 中的 ref

vue
<script setup lang="ts">
import { ref } from 'vue';

const list = ref([1, 2, 3]);
const itemRefs = ref<HTMLLIElement[]>([]);
</script>

<template>
  <ul>
    <li v-for="item in list" :key="item" ref="itemRefs">{{ item }}</li>
  </ul>
</template>

九、常见错误

9.1 模板中写语句

vue
<!-- ❌ 错误 -->
{{ var a = 1 }}
{{ if (ok) { return 'yes' } }}

<!-- ✅ 正确 -->
{{ a + 1 }}
{{ ok ? 'yes' : 'no' }}

9.2 v-for 不加 key

vue
<!-- ❌ 会报警告 -->
<li v-for="item in items">{{ item }}</li>

<!-- ✅ 正确 -->
<li v-for="item in items" :key="item.id">{{ item }}</li>

9.3 ref 在模板中漏 .value

vue
<script setup>
// 在 <script> 中需要 .value
count.value++;

// 在 <template> 中自动解包,不需要 .value
</script>

<template>
  <p>{{ count }}</p>  <!-- ✅ 不用 count.value -->
</template>

十、本章小结

要点关键
插值 文本 / v-html 原始 HTML
属性绑定v-bind / :,支持对象/数组
class/style对象/数组语法
条件渲染v-if 系列 / v-show
列表渲染v-for + :key
事件处理v-on / @,支持修饰符
表单v-model + .lazy/.number/.trim
自定义指令app.directive() 或局部 vXxx

动手练习

  1. 动态 class:实现一个按钮,点击切换 active 状态
  2. 表单:写一个登录表单,验证两次密码一致
  3. 自定义指令:实现 v-copy 一键复制功能
  4. 多 v-model:写一个用户表单组件,支持 name 和 age 两个 v-model

推荐阅读


下一章第 115 章:响应式基础

本站基于 VitePress 构建 · 由 Codebook 团队维护